Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: environment variable to disable plugins #8767

Merged
merged 10 commits into from Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/concepts/plugins.md
Expand Up @@ -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).
geospackle marked this conversation as resolved.
Show resolved Hide resolved

geospackle marked this conversation as resolved.
Show resolved Hide resolved

| Environment Variable | Allowed Values | Description |
|-------------------------------|-------------------------------------------------------|-------------------------------|
| `PYDANTIC_DISABLE_PLUGINS` | `__all__`, `1` | Disables all plugins |
geospackle marked this conversation as resolved.
Show resolved Hide resolved
| | Comma-separated string (e.g. `my-plugin-1,my-plugin2`)| Disables specified plugin(s) |
6 changes: 6 additions & 0 deletions 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

Expand All @@ -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 == '__all__' or disabled_plugins == '1':
return ()
elif _plugins is None:
_plugins = {}
# set _loading_plugins so any plugins that use pydantic don't themselves use plugins
Expand All @@ -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:
Expand Down
74 changes: 74 additions & 0 deletions tests/test_plugin_loader.py
@@ -0,0 +1,74 @@
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(autouse=True)
def reset_plugins():
global loader
loader._plugins = None
yield


@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():
res = loader.get_plugins()
assert list(res) == ['test_plugin:plugin1', 'test_plugin:plugin2', 'test_plugin:plugin3']


def test_disable_all():
os.environ['PYDANTIC_DISABLE_PLUGINS'] = '__all__'
res = loader.get_plugins()
assert res == ()


def test_disable_all_1():
os.environ['PYDANTIC_DISABLE_PLUGINS'] = '1'
res = loader.get_plugins()
assert res == ()


def test_disable_one():
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():
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)