diff --git a/docs/concepts/plugins.md b/docs/concepts/plugins.md index 1e5e054abe..5459199cea 100644 --- a/docs/concepts/plugins.md +++ b/docs/concepts/plugins.md @@ -116,3 +116,12 @@ class Foo(BaseModel, plugin_settings={'observer': 'all'}): ``` On each validation call, the `plugin_settings` will be passed to a callable registered for the events. + +## Disabling Plugins + +You can use environment variable `PYDANTIC_DISABLE_PLUGINS` to disable all or specific plugin(s). + +| Environment Variable | Allowed Values | Description | +|-------------------------------|-------------------------------------------------------|-------------------------------| +| `PYDANTIC_DISABLE_PLUGINS` | `__all__`, `1`, `true` | Disables all plugins | +| | Comma-separated string (e.g. `my-plugin-1,my-plugin2`)| Disables specified plugin(s) | diff --git a/pydantic/plugin/_loader.py b/pydantic/plugin/_loader.py index 9e0e33ca8b..2f90dc541c 100644 --- a/pydantic/plugin/_loader.py +++ b/pydantic/plugin/_loader.py @@ -1,6 +1,7 @@ from __future__ import annotations import importlib.metadata as importlib_metadata +import os import warnings from typing import TYPE_CHECKING, Final, Iterable @@ -22,10 +23,13 @@ def get_plugins() -> Iterable[PydanticPluginProtocol]: Inspired by: https://github.com/pytest-dev/pluggy/blob/1.3.0/src/pluggy/_manager.py#L376-L402 """ + disabled_plugins = os.getenv('PYDANTIC_DISABLE_PLUGINS') global _plugins, _loading_plugins if _loading_plugins: # this happens when plugins themselves use pydantic, we return no plugins return () + elif disabled_plugins in ('__all__', '1', 'true'): + return () elif _plugins is None: _plugins = {} # set _loading_plugins so any plugins that use pydantic don't themselves use plugins @@ -37,6 +41,8 @@ def get_plugins() -> Iterable[PydanticPluginProtocol]: continue if entry_point.value in _plugins: continue + if disabled_plugins is not None and entry_point.name in disabled_plugins.split(','): + continue try: _plugins[entry_point.value] = entry_point.load() except (ImportError, AttributeError) as e: diff --git a/tests/test_plugin_loader.py b/tests/test_plugin_loader.py new file mode 100644 index 0000000000..398ce8e0ff --- /dev/null +++ b/tests/test_plugin_loader.py @@ -0,0 +1,83 @@ +import importlib.metadata as importlib_metadata +import os +from unittest.mock import patch + +import pytest + +import pydantic.plugin._loader as loader + + +class EntryPoint: + def __init__(self, name, value, group): + self.name = name + self.value = value + self.group = group + + def load(self): + return self.value + + +class Dist: + entry_points = [] + + def __init__(self, entry_points): + self.entry_points = entry_points + + +@pytest.fixture +def reset_plugins(): + global loader + initial_plugins = loader._plugins + loader._plugins = None + yield + # teardown + loader._plugins = initial_plugins + + +@pytest.fixture(autouse=True) +def mock(): + mock_entry_1 = EntryPoint(name='test_plugin1', value='test_plugin:plugin1', group='pydantic') + mock_entry_2 = EntryPoint(name='test_plugin2', value='test_plugin:plugin2', group='pydantic') + mock_entry_3 = EntryPoint(name='test_plugin3', value='test_plugin:plugin3', group='pydantic') + mock_dist = Dist([mock_entry_1, mock_entry_2, mock_entry_3]) + + with patch.object(importlib_metadata, 'distributions', return_value=[mock_dist]): + yield + + +def test_loader(reset_plugins): + res = loader.get_plugins() + assert list(res) == ['test_plugin:plugin1', 'test_plugin:plugin2', 'test_plugin:plugin3'] + + +def test_disable_all(reset_plugins): + os.environ['PYDANTIC_DISABLE_PLUGINS'] = '__all__' + res = loader.get_plugins() + assert res == () + + +def test_disable_all_1(reset_plugins): + os.environ['PYDANTIC_DISABLE_PLUGINS'] = '1' + res = loader.get_plugins() + assert res == () + + +def test_disable_true(reset_plugins): + os.environ['PYDANTIC_DISABLE_PLUGINS'] = 'true' + res = loader.get_plugins() + assert res == () + + +def test_disable_one(reset_plugins): + os.environ['PYDANTIC_DISABLE_PLUGINS'] = 'test_plugin1' + res = loader.get_plugins() + assert len(list(res)) == 2 + assert 'test_plugin:plugin1' not in list(res) + + +def test_disable_multiple(reset_plugins): + os.environ['PYDANTIC_DISABLE_PLUGINS'] = 'test_plugin1,test_plugin2' + res = loader.get_plugins() + assert len(list(res)) == 1 + assert 'test_plugin:plugin1' not in list(res) + assert 'test_plugin:plugin2' not in list(res)