Skip to content

Commit b89c80b

Browse files
masenfclaude
authored andcommitted
fix: make disable_plugins accept Plugin types instead of strings (#6155)
* fix: make disable_plugins accept Plugin types instead of strings Change `disable_plugins` from `list[str]` to `list[type[Plugin]]` so both `plugins` and `disable_plugins` use consistent Plugin-based types (#6150). Strings and instances are still accepted at runtime with a deprecation warning, and normalized to the Plugin class. Environment variable support via REFLEX_DISABLE_PLUGINS continues to work through a new `interpret_plugin_class_env` interpreter, which `interpret_plugin_env` now reuses for class resolution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Update reflex/config.py --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 129b904 commit b89c80b

4 files changed

Lines changed: 174 additions & 18 deletions

File tree

reflex/config.py

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,8 @@ class BaseConfig:
259259
# List of plugins to use in the app.
260260
plugins: list[Plugin] = dataclasses.field(default_factory=list)
261261

262-
# List of fully qualified import paths of plugins to disable in the app (e.g. reflex.plugins.sitemap.SitemapPlugin).
263-
disable_plugins: list[str] = dataclasses.field(default_factory=list)
262+
# List of plugin types to disable in the app.
263+
disable_plugins: list[type[Plugin]] = dataclasses.field(default_factory=list)
264264

265265
# The transport method for client-server communication.
266266
transport: Literal["websocket", "polling"] = "websocket"
@@ -358,6 +358,9 @@ def _post_init(self, **kwargs):
358358
for key, env_value in env_kwargs.items():
359359
setattr(self, key, env_value)
360360

361+
# Normalize disable_plugins: convert strings and Plugin subclasses to instances.
362+
self._normalize_disable_plugins()
363+
361364
# Add builtin plugins if not disabled.
362365
if not self._skip_plugins_checks:
363366
self._add_builtin_plugins()
@@ -374,16 +377,52 @@ def _post_init(self, **kwargs):
374377
msg = f"{self._prefixes[0]}REDIS_URL is required when using the redis state manager."
375378
raise ConfigError(msg)
376379

380+
def _normalize_disable_plugins(self):
381+
"""Normalize disable_plugins list entries to Plugin subclasses.
382+
383+
Handles backward compatibility by converting strings (fully qualified
384+
import paths) and Plugin instances to their associated classes.
385+
"""
386+
normalized: list[type[Plugin]] = []
387+
for entry in self.disable_plugins:
388+
if isinstance(entry, type) and issubclass(entry, Plugin):
389+
normalized.append(entry)
390+
elif isinstance(entry, Plugin):
391+
normalized.append(type(entry))
392+
elif isinstance(entry, str):
393+
console.deprecate(
394+
feature_name="Passing strings to disable_plugins",
395+
reason="pass Plugin classes directly instead, e.g. disable_plugins=[SitemapPlugin]",
396+
deprecation_version="0.8.28",
397+
removal_version="0.9.0",
398+
)
399+
try:
400+
from reflex.environment import interpret_plugin_class_env
401+
402+
normalized.append(
403+
interpret_plugin_class_env(entry, "disable_plugins")
404+
)
405+
except Exception:
406+
console.warn(
407+
f"Failed to import plugin from string {entry!r} in disable_plugins. "
408+
"Please pass Plugin subclasses directly.",
409+
)
410+
else:
411+
console.warn(
412+
f"reflex.Config.disable_plugins should contain Plugin subclasses, but got {entry!r}.",
413+
)
414+
self.disable_plugins = normalized
415+
377416
def _add_builtin_plugins(self):
378417
"""Add the builtin plugins to the config."""
379418
for plugin in _PLUGINS_ENABLED_BY_DEFAULT:
380419
plugin_name = plugin.__module__ + "." + plugin.__qualname__
381-
if plugin_name not in self.disable_plugins:
420+
if plugin not in self.disable_plugins:
382421
if not any(isinstance(p, plugin) for p in self.plugins):
383422
console.warn(
384423
f"`{plugin_name}` plugin is enabled by default, but not explicitly added to the config. "
385424
"If you want to use it, please add it to the `plugins` list in your config inside of `rxconfig.py`. "
386-
f"To disable this plugin, set `disable_plugins` to `{[plugin_name, *self.disable_plugins]!r}`.",
425+
f"To disable this plugin, add `{plugin.__name__}` to the `disable_plugins` list.",
387426
)
388427
self.plugins.append(plugin())
389428
else:
@@ -394,16 +433,9 @@ def _add_builtin_plugins(self):
394433
)
395434

396435
for disabled_plugin in self.disable_plugins:
397-
if not isinstance(disabled_plugin, str):
398-
console.warn(
399-
f"reflex.Config.disable_plugins should only contain strings, but got {disabled_plugin!r}. "
400-
)
401-
if not any(
402-
plugin.__module__ + "." + plugin.__qualname__ == disabled_plugin
403-
for plugin in _PLUGINS_ENABLED_BY_DEFAULT
404-
):
436+
if disabled_plugin not in _PLUGINS_ENABLED_BY_DEFAULT:
405437
console.warn(
406-
f"`{disabled_plugin}` is disabled in the config, but it is not a built-in plugin. "
438+
f"`{disabled_plugin!r}` is disabled in the config, but it is not a built-in plugin. "
407439
"Please remove it from the `disable_plugins` list in your config inside of `rxconfig.py`.",
408440
)
409441

reflex/environment.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,17 @@ def interpret_path_env(value: str, field_name: str) -> Path:
149149
return Path(value)
150150

151151

152-
def interpret_plugin_env(value: str, field_name: str) -> Plugin:
153-
"""Interpret a plugin environment variable value.
152+
def interpret_plugin_class_env(value: str, field_name: str) -> type[Plugin]:
153+
"""Interpret an environment variable value as a Plugin subclass.
154+
155+
Resolves a fully qualified import path to the Plugin subclass it refers to.
154156
155157
Args:
156-
value: The environment variable value.
158+
value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin").
157159
field_name: The field name.
158160
159161
Returns:
160-
The interpreted value.
162+
The Plugin subclass.
161163
162164
Raises:
163165
EnvironmentVarValueError: If the value is invalid.
@@ -184,10 +186,30 @@ def interpret_plugin_env(value: str, field_name: str) -> Plugin:
184186
msg = f"Invalid plugin class: {plugin_name!r} for {field_name}. Must be a subclass of Plugin."
185187
raise EnvironmentVarValueError(msg)
186188

189+
return plugin_class
190+
191+
192+
def interpret_plugin_env(value: str, field_name: str) -> Plugin:
193+
"""Interpret a plugin environment variable value.
194+
195+
Resolves a fully qualified import path and returns an instance of the Plugin.
196+
197+
Args:
198+
value: The environment variable value (e.g. "reflex.plugins.sitemap.SitemapPlugin").
199+
field_name: The field name.
200+
201+
Returns:
202+
An instance of the Plugin subclass.
203+
204+
Raises:
205+
EnvironmentVarValueError: If the value is invalid.
206+
"""
207+
plugin_class = interpret_plugin_class_env(value, field_name)
208+
187209
try:
188210
return plugin_class()
189211
except Exception as e:
190-
msg = f"Failed to instantiate plugin {plugin_name!r} for {field_name}: {e}"
212+
msg = f"Failed to instantiate plugin {plugin_class.__name__!r} for {field_name}: {e}"
191213
raise EnvironmentVarValueError(msg) from e
192214

193215

@@ -274,6 +296,14 @@ def interpret_env_var_value(
274296
return interpret_existing_path_env(value, field_name)
275297
if field_type is Plugin:
276298
return interpret_plugin_env(value, field_name)
299+
if get_origin(field_type) is type:
300+
type_args = get_args(field_type)
301+
if (
302+
type_args
303+
and isinstance(type_args[0], type)
304+
and issubclass(type_args[0], Plugin)
305+
):
306+
return interpret_plugin_class_env(value, field_name)
277307
if get_origin(field_type) is Literal:
278308
literal_values = get_args(field_type)
279309
for literal_value in literal_values:

tests/units/test_config.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
interpret_enum_env,
1818
interpret_int_env,
1919
)
20+
from reflex.plugins import Plugin
21+
from reflex.plugins.sitemap import SitemapPlugin
2022

2123

2224
def test_requires_app_name():
@@ -402,3 +404,61 @@ def test_env_file(
402404
)
403405
for key, value in exp_env_vars.items():
404406
assert os.environ.get(key) == value
407+
408+
409+
class TestDisablePlugins:
410+
"""Tests for the disable_plugins config option."""
411+
412+
def test_disable_with_plugin_class(self):
413+
"""Test disabling a plugin by passing the class (type)."""
414+
config = rx.Config(app_name="test", disable_plugins=[SitemapPlugin])
415+
assert not any(isinstance(p, SitemapPlugin) for p in config.plugins)
416+
417+
def test_disable_with_plugin_instance_backward_compat(self):
418+
"""Test disabling a plugin by passing an instance (deprecated)."""
419+
config = rx.Config(app_name="test", disable_plugins=[SitemapPlugin()]) # pyright: ignore[reportArgumentType]
420+
assert not any(isinstance(p, SitemapPlugin) for p in config.plugins)
421+
422+
def test_disable_with_string_backward_compat(self):
423+
"""Test disabling a plugin by passing a string (deprecated)."""
424+
config = rx.Config(
425+
app_name="test",
426+
disable_plugins=["reflex.plugins.sitemap.SitemapPlugin"], # pyright: ignore[reportArgumentType]
427+
)
428+
assert not any(isinstance(p, SitemapPlugin) for p in config.plugins)
429+
430+
def test_disable_plugins_normalized_to_classes(self):
431+
"""Test that disable_plugins entries are normalized to Plugin subclasses."""
432+
config = rx.Config(app_name="test", disable_plugins=[SitemapPlugin])
433+
assert all(
434+
isinstance(dp, type) and issubclass(dp, Plugin)
435+
for dp in config.disable_plugins
436+
)
437+
438+
def test_disable_instance_normalized_to_class(self):
439+
"""Test that a Plugin instance in disable_plugins is normalized to its class."""
440+
config = rx.Config(app_name="test", disable_plugins=[SitemapPlugin()]) # pyright: ignore[reportArgumentType]
441+
assert config.disable_plugins == [SitemapPlugin]
442+
443+
def test_disable_string_normalized_to_class(self):
444+
"""Test that a string in disable_plugins is normalized to the class."""
445+
config = rx.Config(
446+
app_name="test",
447+
disable_plugins=["reflex.plugins.sitemap.SitemapPlugin"], # pyright: ignore[reportArgumentType]
448+
)
449+
assert config.disable_plugins == [SitemapPlugin]
450+
451+
def test_disable_and_plugins_conflict_warns(self):
452+
"""Test that a warning is issued when a plugin is both enabled and disabled."""
453+
config = rx.Config(
454+
app_name="test",
455+
plugins=[SitemapPlugin()],
456+
disable_plugins=[SitemapPlugin],
457+
)
458+
# Plugin should still be in plugins list (just warned)
459+
assert any(isinstance(p, SitemapPlugin) for p in config.plugins)
460+
461+
def test_no_disable_adds_builtin(self):
462+
"""Test that builtin plugins are added when not disabled."""
463+
config = rx.Config(app_name="test")
464+
assert any(isinstance(p, SitemapPlugin) for p in config.plugins)

tests/units/test_environment.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
interpret_existing_path_env,
3131
interpret_int_env,
3232
interpret_path_env,
33+
interpret_plugin_class_env,
3334
interpret_plugin_env,
3435
)
3536
from reflex.plugins import Plugin
@@ -125,6 +126,30 @@ def test_interpret_plugin_env_invalid_class(self):
125126
with pytest.raises(EnvironmentVarValueError, match="Invalid plugin class"):
126127
interpret_plugin_env("tests.units.test_environment.TestEnum", "TEST_FIELD")
127128

129+
def test_interpret_plugin_class_env_valid(self):
130+
"""Test plugin class interpretation returns the class, not an instance."""
131+
result = interpret_plugin_class_env(
132+
"tests.units.test_environment.TestPlugin", "TEST_FIELD"
133+
)
134+
assert result is TestPlugin
135+
136+
def test_interpret_plugin_class_env_invalid_format(self):
137+
"""Test plugin class interpretation with invalid format."""
138+
with pytest.raises(EnvironmentVarValueError, match="Invalid plugin value"):
139+
interpret_plugin_class_env("invalid_format", "TEST_FIELD")
140+
141+
def test_interpret_plugin_class_env_import_error(self):
142+
"""Test plugin class interpretation with import error."""
143+
with pytest.raises(EnvironmentVarValueError, match="Failed to import module"):
144+
interpret_plugin_class_env("non.existent.module.Plugin", "TEST_FIELD")
145+
146+
def test_interpret_plugin_class_env_invalid_class(self):
147+
"""Test plugin class interpretation with invalid class."""
148+
with pytest.raises(EnvironmentVarValueError, match="Invalid plugin class"):
149+
interpret_plugin_class_env(
150+
"tests.units.test_environment.TestEnum", "TEST_FIELD"
151+
)
152+
128153
def test_interpret_enum_env_valid(self):
129154
"""Test enum interpretation with valid values."""
130155
result = interpret_enum_env("value1", _TestEnum, "TEST_FIELD")
@@ -172,6 +197,15 @@ def test_interpret_plugin(self):
172197
)
173198
assert isinstance(result, TestPlugin)
174199

200+
def test_interpret_plugin_class(self):
201+
"""Test type[Plugin] interpretation returns the class."""
202+
result = interpret_env_var_value(
203+
"tests.units.test_environment.TestPlugin",
204+
type[Plugin],
205+
"TEST_FIELD",
206+
)
207+
assert result is TestPlugin
208+
175209
def test_interpret_list(self):
176210
"""Test list interpretation."""
177211
result = interpret_env_var_value("1:2:3", list[int], "TEST_FIELD")

0 commit comments

Comments
 (0)