Skip to content

Commit 0e04fb1

Browse files
fix(config): normalize Config.plugins entries with clear errors (#6440) (#6445)
* fix(config): normalize plugins entries with clear errors (#6440) Passing a Plugin class instead of an instance to rx.Config(plugins=...) previously failed deep in the compiler with: TypeError: Plugin.get_stylesheet_paths() missing 1 required positional argument: 'self' Now Config._normalize_plugins() runs in _post_init and: - auto-instantiates bare Plugin subclasses (matches disable_plugins semantics), so plugins=[SitemapPlugin] behaves like plugins=[SitemapPlugin()] - raises ConfigError naming the offending entry for non-Plugin values - raises ConfigError naming the class for Plugin subclasses whose __init__ requires arguments Closes #6440 * fix(config): address review feedback on plugins normalization - Reword TypeError message to be less prescriptive about cause - Flatten TestPluginsNormalization class into module-level test functions per repo convention
1 parent 9f3c9e8 commit 0e04fb1

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

packages/reflex-base/src/reflex_base/config.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,9 @@ def _post_init(self, **kwargs):
348348
for key, env_value in env_kwargs.items():
349349
setattr(self, key, env_value)
350350

351+
# Normalize plugins: auto-instantiate Plugin subclasses, reject bad values.
352+
self._normalize_plugins()
353+
351354
# Normalize disable_plugins: convert strings and Plugin subclasses to instances.
352355
self._normalize_disable_plugins()
353356

@@ -382,6 +385,39 @@ def _post_init(self, **kwargs):
382385
msg = f"{self._prefixes[0]}REDIS_URL is required when using the redis state manager."
383386
raise ConfigError(msg)
384387

388+
def _normalize_plugins(self):
389+
"""Normalize ``plugins`` entries to Plugin instances.
390+
391+
Auto-instantiates Plugin subclasses passed without parentheses (e.g.
392+
``plugins=[SitemapPlugin]``) so they behave the same as
393+
``plugins=[SitemapPlugin()]``. Any entry that is neither a Plugin
394+
subclass nor a Plugin instance raises ``ConfigError`` with a message
395+
that names the offending value, instead of failing later in the
396+
compiler with a confusing ``TypeError`` about a missing ``self``.
397+
"""
398+
normalized: list[Plugin] = []
399+
for entry in self.plugins:
400+
if isinstance(entry, Plugin):
401+
normalized.append(entry)
402+
elif isinstance(entry, type) and issubclass(entry, Plugin):
403+
try:
404+
normalized.append(entry())
405+
except TypeError as exc:
406+
msg = (
407+
f"reflex.Config.plugins entry {entry.__name__!r} could not be "
408+
f"instantiated and may require arguments; pass an instance "
409+
f"instead, e.g. plugins=[{entry.__name__}(...)]."
410+
)
411+
raise ConfigError(msg) from exc
412+
else:
413+
msg = (
414+
f"reflex.Config.plugins must contain Plugin instances, but got "
415+
f"{entry!r} of type {type(entry).__name__}. "
416+
f"Pass an instance, e.g. plugins=[SitemapPlugin()]."
417+
)
418+
raise ConfigError(msg)
419+
self.plugins = normalized
420+
385421
def _normalize_disable_plugins(self):
386422
"""Normalize disable_plugins list entries to Plugin subclasses.
387423

tests/units/test_config.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from reflex_base.constants import Endpoint, Env
1010
from reflex_base.plugins import Plugin
1111
from reflex_base.plugins.sitemap import SitemapPlugin
12+
from reflex_base.utils.exceptions import ConfigError
1213

1314
import reflex as rx
1415
from reflex.environment import (
@@ -557,3 +558,36 @@ def test_no_disable_adds_builtin(self):
557558
"""Test that builtin plugins are added when not disabled."""
558559
config = rx.Config(app_name="test")
559560
assert any(isinstance(p, SitemapPlugin) for p in config.plugins)
561+
562+
563+
def test_plugins_instance_passthrough():
564+
"""A Plugin instance is kept as-is (issue #6440)."""
565+
instance = SitemapPlugin()
566+
config = rx.Config(app_name="test", plugins=[instance])
567+
assert instance in config.plugins
568+
569+
570+
def test_plugins_class_auto_instantiated():
571+
"""A Plugin subclass is auto-instantiated rather than raising deep in the compiler (issue #6440)."""
572+
config = rx.Config(app_name="test", plugins=[SitemapPlugin]) # pyright: ignore[reportArgumentType]
573+
instances = [p for p in config.plugins if isinstance(p, SitemapPlugin)]
574+
assert len(instances) == 1
575+
# And it must be an instance, not the class itself.
576+
assert not isinstance(instances[0], type)
577+
578+
579+
def test_plugins_invalid_value_raises_config_error():
580+
"""A non-Plugin value raises ConfigError naming the entry, not a deep TypeError (issue #6440)."""
581+
with pytest.raises(ConfigError, match=r"reflex\.Config\.plugins"):
582+
rx.Config(app_name="test", plugins=["not-a-plugin"]) # pyright: ignore[reportArgumentType]
583+
584+
585+
def test_plugins_class_requiring_args_raises_config_error():
586+
"""A Plugin subclass that needs constructor args raises a clear ConfigError (issue #6440)."""
587+
588+
class NeedsArgs(Plugin):
589+
def __init__(self, required):
590+
self.required = required
591+
592+
with pytest.raises(ConfigError, match="NeedsArgs"):
593+
rx.Config(app_name="test", plugins=[NeedsArgs]) # pyright: ignore[reportArgumentType]

0 commit comments

Comments
 (0)