diff --git a/news/+warning-escape-leak.bugfix.md b/news/+warning-escape-leak.bugfix.md new file mode 100644 index 00000000000..6d1b196d52f --- /dev/null +++ b/news/+warning-escape-leak.bugfix.md @@ -0,0 +1 @@ +Console warnings and errors no longer print literal backslash-escaped brackets (e.g. `dict\[str, str]`). The rich-markup escapes were left over from the legacy console helpers, but the logging pipeline renders messages with markup disabled, so bracketed type names now print verbatim. `VarAttributeError` messages drop the same escapes. diff --git a/packages/reflex-base/news/+warning-escape-leak.bugfix.md b/packages/reflex-base/news/+warning-escape-leak.bugfix.md new file mode 100644 index 00000000000..6d1b196d52f --- /dev/null +++ b/packages/reflex-base/news/+warning-escape-leak.bugfix.md @@ -0,0 +1 @@ +Console warnings and errors no longer print literal backslash-escaped brackets (e.g. `dict\[str, str]`). The rich-markup escapes were left over from the legacy console helpers, but the logging pipeline renders messages with markup disabled, so bracketed type names now print verbatim. `VarAttributeError` messages drop the same escapes. diff --git a/packages/reflex-base/src/reflex_base/components/component.py b/packages/reflex-base/src/reflex_base/components/component.py index 80345c22e59..6bb6888ccc5 100644 --- a/packages/reflex-base/src/reflex_base/components/component.py +++ b/packages/reflex-base/src/reflex_base/components/component.py @@ -19,7 +19,6 @@ from types import SimpleNamespace from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast -from rich.markup import escape from typing_extensions import dataclass_transform from reflex_base import constants @@ -586,7 +585,7 @@ def satisfies_type_hint(obj: Any, type_hint: Any) -> bool: ) logger.warning( "Passing None to a Var that is not explicitly marked as Optional (| None) is deprecated. " - f"Passed {obj!s} of type {escape(str(type(obj) if not isinstance(obj, Var) else obj._var_type))} to {escape(str(type_hint))}." + f"Passed {obj!s} of type {type(obj) if not isinstance(obj, Var) else obj._var_type} to {type_hint}." ) return True return False diff --git a/packages/reflex-base/src/reflex_base/event/__init__.py b/packages/reflex-base/src/reflex_base/event/__init__.py index 6f0f0d9238b..b191bd93349 100644 --- a/packages/reflex-base/src/reflex_base/event/__init__.py +++ b/packages/reflex-base/src/reflex_base/event/__init__.py @@ -2021,14 +2021,12 @@ def _check_event_args_subclass_of_callback( for arg in args ] - expect_string = ", ".join( - repr(arg) for arg in args_types_without_vars - ).replace("[", "\\[") + expect_string = ", ".join(repr(arg) for arg in args_types_without_vars) given_string = ", ".join( repr(callback_param_name_to_type.get(arg, Any)) for arg in callback_params_names - ).replace("[", "\\[") + ) as_annotated_in = ( f" as annotated in {callback_name}" if callback_name else "" diff --git a/packages/reflex-base/src/reflex_base/event/processor/event_processor.py b/packages/reflex-base/src/reflex_base/event/processor/event_processor.py index fcdc90eade5..4f106c44bb9 100644 --- a/packages/reflex-base/src/reflex_base/event/processor/event_processor.py +++ b/packages/reflex-base/src/reflex_base/event/processor/event_processor.py @@ -14,7 +14,6 @@ from contextvars import Token, copy_context from typing import TYPE_CHECKING, Any, TypeVar -import rich.markup from typing_extensions import Self from reflex.app_mixins.middleware import MiddlewareMixin @@ -749,9 +748,7 @@ async def _process_queue(self): except Exception: # Log the error and continue processing the next events. logger.exception( - rich.markup.escape( - f"Error processing event queue entry for {entry.event} [txid={entry.ctx.txid}]:" - ) + f"Error processing event queue entry for {entry.event} [txid={entry.ctx.txid}]:" ) queue.task_done() if self._queue_task is asyncio.current_task(): diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 676bf16a8d6..8803960d51c 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -38,7 +38,6 @@ overload, ) -from rich.markup import escape from typing_extensions import LiteralString, dataclass_transform, override from reflex_base import constants @@ -1484,7 +1483,7 @@ def __getattr__(self, name: str): f"access the attribute '{name}'", ) - msg = f"The State var {escape(self._js_expr)} of type {escape(str(self._var_type))} has no attribute '{name}' or may have been annotated wrongly." + msg = f"The State var {self._js_expr} of type {self._var_type} has no attribute '{name}' or may have been annotated wrongly." raise VarAttributeError(msg) def __bool__(self) -> bool: @@ -2613,7 +2612,7 @@ def _check_deprecated_return_type(self, instance: BaseState, value: Any) -> None if not _isinstance(value, self._var_type, nested=1, treat_var_as_type=False): logger.error( f"Computed var '{type(instance).__name__}.{self._name}' must return" - f" a value of type '{escape(str(self._var_type))}', got '{value!s}' of type {type(value)}." + f" a value of type '{self._var_type}', got '{value!s}' of type {type(value)}." ) def _deps( diff --git a/packages/reflex-base/src/reflex_base/vars/object.py b/packages/reflex-base/src/reflex_base/vars/object.py index bf10fdbb993..ab809eeb19f 100644 --- a/packages/reflex-base/src/reflex_base/vars/object.py +++ b/packages/reflex-base/src/reflex_base/vars/object.py @@ -17,8 +17,6 @@ overload, ) -from rich.markup import escape - from reflex_base.utils import types from reflex_base.utils.exceptions import VarAttributeError from reflex_base.utils.types import ( @@ -348,7 +346,7 @@ def __getattr__(self, name: str) -> Var: if attribute_type is None: msg = ( - f"The State var `{self!s}` of type {escape(str(self._var_type))} has no attribute '{name}' or may have been annotated " + f"The State var `{self!s}` of type {self._var_type} has no attribute '{name}' or may have been annotated " f"wrongly." ) raise VarAttributeError(msg) diff --git a/reflex/state.py b/reflex/state.py index 452c80e7109..15722061c65 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -813,7 +813,7 @@ def computed_var_func(state: Self): if not _isinstance(result, of_type, nested=1, treat_var_as_type=False): logger.warning( - f"Inline ComputedVar {f} expected type {escape(str(of_type))}, got {type(result)}. " + f"Inline ComputedVar {f} expected type {of_type}, got {type(result)}. " "You can specify expected type with `of_type` argument." ) @@ -1543,7 +1543,7 @@ def __setattr__(self, name: str, value: Any): field_type = field.outer_type_ if not _isinstance(value, field_type, nested=1, treat_var_as_type=False): logger.error( - f"Expected field '{type(self).__name__}.{name}' to receive type '{escape(str(field_type))}'," + f"Expected field '{type(self).__name__}.{name}' to receive type '{field_type}'," f" but got '{value}' of type '{type(value)}'." ) diff --git a/tests/units/reflex_base/utils/test_log.py b/tests/units/reflex_base/utils/test_log.py index f204943012b..698a58c3368 100644 --- a/tests/units/reflex_base/utils/test_log.py +++ b/tests/units/reflex_base/utils/test_log.py @@ -71,6 +71,18 @@ def test_markup_literal_by_default(capsys): assert out == "Info: AssertionErr: foo[bar] != 'baz'\n" +def test_warning_with_markup_tags_stays_literal(capsys): + """User data resembling rich markup is neither styled nor escaped. + + Messages must not be markup-escaped at the call site: the sink renders + them raw, so the escape backslashes would print literally, while markup + injection stays impossible because markup is off by default. + """ + logger.warning("got [red]x[/red] of type dict[str, str]") + out, _ = capsys.readouterr() + assert out == "Warning: got [red]x[/red] of type dict[str, str]\n" + + def test_level_gating(capsys): """Records below the configured level are dropped.""" log.set_log_level(LogLevel.WARNING) diff --git a/tests/units/test_event.py b/tests/units/test_event.py index fa07a89dcc6..7194b746641 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -3,6 +3,7 @@ from typing import Any, cast import pytest +from reflex_base.constants import LogLevel from reflex_base.constants.compiler import Hooks, Imports from reflex_base.event import ( BACKGROUND_TASK_MARKER, @@ -15,10 +16,13 @@ call_event_handler, event, fix_events, + on_submit_event, + on_submit_string_event, ) -from reflex_base.utils import format +from reflex_base.utils import format, log from reflex_base.utils.exceptions import EventHandlerValueError from reflex_base.vars.base import Field, LiteralVar, Var, field +from rich.console import Console import reflex as rx from reflex.state import BaseState @@ -1281,3 +1285,33 @@ def test_decentralized_event_global_state(): """Test the decentralized event with a global state.""" _ = rx.input(on_change=f("foo")) _ = rx.input(on_change=f) + + +def test_arg_mismatch_warning_renders_brackets_verbatim(capsys, monkeypatch): + """The arg-mismatch warning renders bracketed type names without escapes. + + The rich console sink prints records with markup disabled, so the message + must not carry rich-markup escapes: the backslashes would print literally. + """ + + def handle_submit(form_data: dict[str, str]): + pass + + handle_submit.__qualname__ = "handle_submit" + + monkeypatch.setenv(log._MANAGED_ENV_VAR, "true") + monkeypatch.setattr(log, "_log_level", LogLevel.INFO) + # A wide console so the long warning is not line-wrapped mid-assertion. + monkeypatch.setattr(log, "_console", Console(highlight=False, width=1000)) + log.configure() + try: + call_event_handler( + EventHandler(fn=handle_submit), + (on_submit_event, on_submit_string_event), + key="on_submit", + ) + out, _ = capsys.readouterr() + finally: + log._reset() + assert "expects (dict[str, typing.Any]) -> () but got (dict[str, str]) -> ()" in out + assert "\\" not in out