Describe the bug
The __rich_repr__() protocol allows the following types to be yielded:
tuple[None, Any] - positional, safe form
tuple[str, Any] - keyword
tuple[None, Any, Any] - positional with default
tuple[str, Any, Any] - keyword with default
Any - positional, unsafe form
That the protocol accepts (5: Any) easily leads to errors when the Any in question happens to be a tuple (an escaping and injection problem).
Example
This project's own @auto decorator makes this mistake (c.f.: #4014, #4016), leading to TypeErrors later in rendering with no obvious cause or fix.
Possibilities for addressing this
One way to mitigate the issue would be to require/encourage yielding positionals in form 1 (tuple[None, Any]) rather than form 5 (Any). (This might take the form of documentation, deprecation, facilitating static type checks, opt-in runtime safety checks, any combination of these, etc.)
Static type checking
In repr.py, Result is currently defined as follows:
Result = Iterable[Union[Any, Tuple[Any], Tuple[str, Any], Tuple[str, Any, Any]]]
Since the Union includes Any, in practice, any iterator (generator function) will pass the type check.
Aside: In fact, the Result definition is incomplete/incorrect in that it doesn't spell out that forms 1 (Tuple[None, Any]) and 3 (Tuple[None, Any, Any]) are perfectly valid. I would guess this has gone unnoticed because the dangerously-broad form 5 (Any) happens to cover these cases.
It might be helpful to add:
ResultSafe = Iterable[tuple[str | None, Any], tuple[str | None, Any, Any]]
Developers who use ResultSafe rather than Result as the return annotation when writing a __rich_repr__() will benefit from warnings about most potentially-hazardous yield statements.
Runtime type checking
In the simplest case, developers would opt-in to yielding only tuples (valid ResultSafevalues, as defined above). If anything other than a tuple is yielded, it should be treated as an error. This won't help when the unescaped value actually is a tuple, but it will help developers maintain good discipline and perhaps detect some cases where the value is not always a tuple.
It might even be helpful to declare and optionally require a specific class to be used rather than tuple, so the yielded values will never be valid by coincidence. This would probably have fields keyword: str | None, value: Any, and something like default: Any | <SomeSentinel>. It might be a NamedTuple, or even just a trivial tuple subclass.
Implementations might opt-in to runtime type checks by decorator, or (though it's a bit hacky) yielding a RichReprConfig object before anything else. Aside: In either case, it might be helpful to have a single decorator for __rich_repr__()s which also accepts angular: bool = False. Some type checkers, e.g., Pyright, will complain about setting undeclared attributes on FunctionTypes, making __rich_repr__.angular = True a source of noise; a decorator to set angular would provide a graceful workaround.)
Combining with static typing
The decorator might require the decorated __rich_repr__() implementation to be annotated as returning ResultSafe. A @deprecated @overload (so the old protocol would work, but trigger a type checker warning) would be preferable, but decorator factories (as opposed to parameterless decorators) can't effectively leverage @deprecated yet, at least in pyright: microsoft/pyright#11292.
Deprecation
It may be desirable to gradually deprecate form 5 (Any), perhaps by issuing a warning when a non-tuple is yielded from __rich_repr__() or when no safer-form-only opt-in signal is found.
Documentation
In my opinion, documentation should be amended strongly discourage form 5 (Any), and to encourage use of whatever safeguards the Textual/Rich team opts to provide.
Platform
All affected. Details omitted.
Describe the bug
The
__rich_repr__()protocol allows the following types to be yielded:tuple[None, Any]- positional, safe formtuple[str, Any]- keywordtuple[None, Any, Any]- positional with defaulttuple[str, Any, Any]- keyword with defaultAny- positional, unsafe formThat the protocol accepts (5:
Any) easily leads to errors when theAnyin question happens to be atuple(an escaping and injection problem).Example
This project's own
@autodecorator makes this mistake (c.f.: #4014, #4016), leading toTypeErrors later in rendering with no obvious cause or fix.Possibilities for addressing this
One way to mitigate the issue would be to require/encourage yielding positionals in form 1 (
tuple[None, Any]) rather than form 5 (Any). (This might take the form of documentation, deprecation, facilitating static type checks, opt-in runtime safety checks, any combination of these, etc.)Static type checking
In
repr.py,Resultis currently defined as follows:Result = Iterable[Union[Any, Tuple[Any], Tuple[str, Any], Tuple[str, Any, Any]]]Since the
UnionincludesAny, in practice, any iterator (generator function) will pass the type check.Aside: In fact, the
Resultdefinition is incomplete/incorrect in that it doesn't spell out that forms 1 (Tuple[None, Any]) and 3 (Tuple[None, Any, Any]) are perfectly valid. I would guess this has gone unnoticed because the dangerously-broad form 5 (Any) happens to cover these cases.It might be helpful to add:
ResultSafe = Iterable[tuple[str | None, Any], tuple[str | None, Any, Any]]Developers who use
ResultSaferather thanResultas the return annotation when writing a__rich_repr__()will benefit from warnings about most potentially-hazardousyieldstatements.Runtime type checking
In the simplest case, developers would opt-in to yielding only
tuples(validResultSafevalues, as defined above). If anything other than atupleis yielded, it should be treated as an error. This won't help when the unescaped value actually is atuple, but it will help developers maintain good discipline and perhaps detect some cases where the value is not always atuple.It might even be helpful to declare and optionally require a specific class to be used rather than
tuple, so theyielded values will never be valid by coincidence. This would probably have fieldskeyword: str | None,value: Any, and something likedefault: Any | <SomeSentinel>. It might be aNamedTuple, or even just a trivialtuplesubclass.Implementations might opt-in to runtime type checks by decorator, or (though it's a bit hacky) yielding a
RichReprConfigobject before anything else. Aside: In either case, it might be helpful to have a single decorator for__rich_repr__()s which also acceptsangular: bool = False. Some type checkers, e.g., Pyright, will complain about setting undeclared attributes onFunctionTypes, making__rich_repr__.angular = Truea source of noise; a decorator to setangularwould provide a graceful workaround.)Combining with static typing
The decorator might require the decorated
__rich_repr__()implementation to be annotated as returningResultSafe. A@deprecated@overload(so the old protocol would work, but trigger a type checker warning) would be preferable, but decorator factories (as opposed to parameterless decorators) can't effectively leverage@deprecatedyet, at least in pyright: microsoft/pyright#11292.Deprecation
It may be desirable to gradually deprecate form 5 (
Any), perhaps by issuing a warning when a non-tupleis yielded from__rich_repr__()or when no safer-form-only opt-in signal is found.Documentation
In my opinion, documentation should be amended strongly discourage form 5 (
Any), and to encourage use of whatever safeguards the Textual/Rich team opts to provide.Platform
All affected. Details omitted.