diff --git a/news/+bundled-libraries-shim.deprecation.md b/news/+bundled-libraries-shim.deprecation.md new file mode 100644 index 00000000000..c36ec026e24 --- /dev/null +++ b/news/+bundled-libraries-shim.deprecation.md @@ -0,0 +1 @@ +`reflex.components.dynamic.bundled_libraries` and `DEFAULT_BUNDLED_LIBRARIES` are deprecated (removal in 1.0) but keep working: reading either emits a deprecation warning and resolves against the active `RegistrationContext`. Use `RegistrationContext.ensure_context().bundled_libraries` to read the list, or `bundle_library()` / `reset_bundled_libraries()` to modify it. diff --git a/packages/reflex-base/news/6382.breaking.md b/packages/reflex-base/news/6382.breaking.md index 9bdd6d222d3..3a6d9df7cba 100644 --- a/packages/reflex-base/news/6382.breaking.md +++ b/packages/reflex-base/news/6382.breaking.md @@ -1 +1 @@ -`get_config(reload=True)` has been replaced by `reload_config()`, and the module-level `bundled_libraries` list in `reflex_base.components.dynamic` has moved onto the active `RegistrationContext` (use `bundle_library()` / `reset_bundled_libraries()` as before). +`get_config(reload=True)` has been replaced by `reload_config()`, and the module-level `bundled_libraries` list in `reflex_base.components.dynamic` has moved onto the active `RegistrationContext` (use `bundle_library()` / `reset_bundled_libraries()` as before). Reading `reflex_base.components.dynamic.bundled_libraries` (or `DEFAULT_BUNDLED_LIBRARIES`) still works as a deprecated shim that resolves against the active context; the shims are removed in 1.0. diff --git a/packages/reflex-base/src/reflex_base/components/dynamic.py b/packages/reflex-base/src/reflex_base/components/dynamic.py index e5b941b270d..1f15953fd36 100644 --- a/packages/reflex-base/src/reflex_base/components/dynamic.py +++ b/packages/reflex-base/src/reflex_base/components/dynamic.py @@ -1,10 +1,10 @@ """Components that are dynamically generated on the backend.""" -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, Any, Union from reflex_base import constants from reflex_base.registry import RegistrationContext, _default_bundled_libraries -from reflex_base.utils import imports +from reflex_base.utils import console, imports from reflex_base.utils.exceptions import DynamicComponentMissingLibraryError from reflex_base.utils.format import format_library_name from reflex_base.utils.serializers import serializer @@ -15,6 +15,48 @@ from reflex_base.components.component import Component +def __getattr__(name: str) -> Any: + """Provide the module-level globals that moved onto `RegistrationContext`. + + Kept so downstream packages pinned to an older Reflex (notably + reflex-enterprise, which reads `dynamic.bundled_libraries`) keep working. + + Args: + name: The name of the attribute to look up. + + Returns: + The relocated value, resolved against the active `RegistrationContext`. + + Raises: + AttributeError: If the attribute is not a relocated global. + """ + if name == "bundled_libraries": + console.deprecate( + feature_name="reflex_base.components.dynamic.bundled_libraries", + reason=( + "The bundled library list now lives on the active RegistrationContext. " + "Use RegistrationContext.ensure_context().bundled_libraries to read it, " + "or bundle_library()/reset_bundled_libraries() to modify it" + ), + deprecation_version="0.9.9", + removal_version="1.0", + ) + return RegistrationContext.ensure_context().bundled_libraries + if name == "DEFAULT_BUNDLED_LIBRARIES": + console.deprecate( + feature_name="reflex_base.components.dynamic.DEFAULT_BUNDLED_LIBRARIES", + reason=( + "Every RegistrationContext starts with these libraries bundled; call " + "reset_bundled_libraries() to restore them on the active context" + ), + deprecation_version="0.9.9", + removal_version="1.0", + ) + return _default_bundled_libraries() + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) + + def get_cdn_url(lib: str) -> str: """Get the CDN URL for a library. diff --git a/reflex/components/dynamic.py b/reflex/components/dynamic.py index f2612eb8532..0f3a260e86b 100644 --- a/reflex/components/dynamic.py +++ b/reflex/components/dynamic.py @@ -1,4 +1,27 @@ # pyright: reportWildcardImportFromLibrary=false """Re-export from reflex_base.""" +from typing import Any + from reflex_base.components.dynamic import * # pragma: no cover + + +def __getattr__(name: str) -> Any: + """Delegate to `reflex_base.components.dynamic` for names the star import misses. + + Args: + name: The name of the attribute to look up. + + Returns: + The attribute from the re-exported module. + + Raises: + AttributeError: If the re-exported module has no such attribute. + """ + from reflex_base.components import dynamic + + try: + return getattr(dynamic, name) + except AttributeError: + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) from None diff --git a/tests/units/reflex_base/components/__init__.py b/tests/units/reflex_base/components/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/units/reflex_base/components/test_dynamic.py b/tests/units/reflex_base/components/test_dynamic.py new file mode 100644 index 00000000000..d73547da83e --- /dev/null +++ b/tests/units/reflex_base/components/test_dynamic.py @@ -0,0 +1,52 @@ +"""Tests for the compatibility shims in reflex_base.components.dynamic.""" + +import importlib + +import pytest +from reflex_base.components import dynamic +from reflex_base.registry import RegistrationContext, _default_bundled_libraries + +from reflex.components import dynamic as reflex_dynamic + + +def test_bundled_libraries_shim_returns_active_context_list(): + """The module-level `bundled_libraries` resolves against the active context.""" + with RegistrationContext() as ctx: + assert dynamic.bundled_libraries is ctx.bundled_libraries + dynamic.bundle_library("some-shimmed-lib") + assert "some-shimmed-lib" in dynamic.bundled_libraries + + with RegistrationContext(): + assert "some-shimmed-lib" not in dynamic.bundled_libraries + + +def test_bundled_libraries_shim_via_reflex_namespace(): + """reflex-enterprise reads the shim off `reflex.components.dynamic`.""" + with RegistrationContext() as ctx: + assert set(reflex_dynamic.bundled_libraries) == set(ctx.bundled_libraries) + + +def test_default_bundled_libraries_shim(): + """The `DEFAULT_BUNDLED_LIBRARIES` shim returns the default library list.""" + assert _default_bundled_libraries() == dynamic.DEFAULT_BUNDLED_LIBRARIES + + +def test_bundled_libraries_shim_warns(mocker): + """Reading a relocated global emits a deprecation warning.""" + deprecate = mocker.patch("reflex_base.utils.console.deprecate") + + with RegistrationContext(): + _ = dynamic.bundled_libraries + deprecate.assert_called_once() + assert ( + deprecate.call_args.kwargs["feature_name"] + == "reflex_base.components.dynamic.bundled_libraries" + ) + + +@pytest.mark.parametrize("module_name", ["reflex_base", "reflex"]) +def test_unknown_attribute_raises(module_name: str): + """Unknown attributes still raise AttributeError naming the module.""" + module = importlib.import_module(f"{module_name}.components.dynamic") + with pytest.raises(AttributeError, match=f"{module.__name__!r}"): + _ = module.definitely_not_an_attribute