Source: textual
Version: 2.1.2-1
Severity: serious
Tags: ftbfs, patch
rich 13.9.4-1.1 causes one test failure in textual:
__________________________________ test_demo ___________________________________
async def test_demo():
"""Test the demo runs."""
# Test he demo can at least run.
# This exists mainly to catch screw-ups that might effect only certain
Python versions.
app = DemoApp()
> async with app.run_test() as pilot:
^^^^^^^^^^^^^^
tests/test_demo.py:9:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/usr/lib/python3.13/contextlib.py:221: in __aexit__
await anext(self.gen)
/usr/lib/python3/dist-packages/textual/app.py:1991: in run_test
raise self._exception
/usr/lib/python3/dist-packages/textual/message_pump.py:609: in
_process_messages_loop
await self._dispatch_message(message)
/usr/lib/python3/dist-packages/textual/message_pump.py:673: in _dispatch_message
await self.on_event(message)
/usr/lib/python3/dist-packages/textual/message_pump.py:754: in on_event
await self._on_message(event)
/usr/lib/python3/dist-packages/textual/message_pump.py:775: in _on_message
await invoke(method, message)
/usr/lib/python3/dist-packages/textual/_callback.py:93: in invoke
return await _invoke(callback, *params)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/usr/lib/python3/dist-packages/textual/_callback.py:53: in _invoke
result = callback(*params[:parameter_count])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/usr/lib/python3/dist-packages/textual/screen.py:1335: in _on_screen_resume
self._refresh_layout(size)
/usr/lib/python3/dist-packages/textual/screen.py:1255: in _refresh_layout
self._compositor_refresh()
/usr/lib/python3/dist-packages/textual/screen.py:1091: in _compositor_refresh
update = self._compositor.render_update(
/usr/lib/python3/dist-packages/textual/_compositor.py:1082: in render_update
return self.render_full_update(simplify=simplify)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/usr/lib/python3/dist-packages/textual/_compositor.py:1118: in
render_full_update
chops = self._render_chops(crop, lambda y: True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/usr/lib/python3/dist-packages/textual/_compositor.py:1188: in _render_chops
for region, clip, strips in renders:
^^^^^^^
/usr/lib/python3/dist-packages/textual/_compositor.py:1037: in _get_renders
widget.render_lines(
/usr/lib/python3/dist-packages/textual/widget.py:3919: in render_lines
strips = self._styles_cache.render_widget(self, crop)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/usr/lib/python3/dist-packages/textual/_styles_cache.py:127: in render_widget
strips = self.render(
/usr/lib/python3/dist-packages/textual/_styles_cache.py:230: in render
strip = render_line(
/usr/lib/python3/dist-packages/textual/_styles_cache.py:449: in render_line
line = render_content_line(y - gutter.top)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/usr/lib/python3/dist-packages/textual/widget.py:3902: in render_line
self._render_content()
/usr/lib/python3/dist-packages/textual/widget.py:3888: in _render_content
strips = Visual.to_strips(self, visual, width, height, self.visual_style)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/usr/lib/python3/dist-packages/textual/visual.py:199: in to_strips
strips = visual.render_strips(
/usr/lib/python3/dist-packages/textual/content.py:520: in render_strips
strip_lines = [Strip(*line.to_strip(style)) for line in lines]
^^^^^^^^^^^^^^^^^^^^
/usr/lib/python3/dist-packages/textual/content.py:1376: in to_strip
_Segment(text, (style + text_style).rich_style_with_offset(x, y))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/usr/lib/python3/dist-packages/textual/style.py:365: in rich_style_with_offset
meta={**self.meta, "offset": (x, y)},
^^^^^^^^^
/usr/lib/python3.13/functools.py:1025: in __get__
val = self.func(instance)
^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <[ValueError('bad marshal data (unknown type code)') raised in repr()]
Style object at 0x7fa0743a8050>
@cached_property
def meta(self) -> Mapping[str, Any]:
"""Get meta information (can not be changed after construction)."""
> return {} if self._meta is None else loads(self._meta)
^^^^^^^^^^^^^^^^^
E ValueError: bad marshal data (unknown type code)
/usr/lib/python3/dist-packages/textual/style.py:396: ValueError
This was my NMU, so I'm sorry for not spotting the problem in advance; I
thought the rich changes were small enough to avoid compatibility issues
in textual and missed the fact that they switched from marshal to pickle
for style meta information. However, I don't think the compatibility
breakage could really have been avoided anyway: the change that caused
this was part of upstream changes to support Python 3.14, and when I
experimented locally I also couldn't find a way to get marshal.dumps to
do the right thing on Python 3.14 so I can absolutely see why they
switched to pickle instead.
The corresponding upstream fix to textual is in a commit unhelpfully
labelled just "optimization"
(https://github.com/Textualize/textual/pull/6169); as far as I can see
upstream failed to label either side of this as affecting compatibility
between rich and textual. I think the only sensible way forward is to
cherry-pick the relevant part of the textual change as well, add a
Depends to textual for the new rich, and add a Breaks on old textual
versions to rich.
https://salsa.debian.org/morph/textual/-/merge_requests/1 has the
necessary changes to textual, and I've attached a debdiff here too. The
change to rich would then just be adding "Breaks: python3-textual (<<
2.1.2-1.1~)".
Thanks,
--
Colin Watson (he/him) [[email protected]]
diff -Nru textual-2.1.2/debian/changelog textual-2.1.2/debian/changelog
--- textual-2.1.2/debian/changelog 2025-03-16 21:27:22.000000000 +0000
+++ textual-2.1.2/debian/changelog 2026-01-12 11:18:59.000000000 +0000
@@ -1,3 +1,10 @@
+textual (2.1.2-1.1) UNRELEASED; urgency=medium
+
+ * Non-maintainer upload.
+ * Use pickle for style meta information to match rich 13.9.4-1.1.
+
+ -- Colin Watson <[email protected]> Mon, 12 Jan 2026 11:18:59 +0000
+
textual (2.1.2-1) unstable; urgency=medium
* New upstream release
diff -Nru textual-2.1.2/debian/control textual-2.1.2/debian/control
--- textual-2.1.2/debian/control 2025-03-16 21:27:22.000000000 +0000
+++ textual-2.1.2/debian/control 2026-01-12 11:18:59.000000000 +0000
@@ -12,7 +12,7 @@
python3-pytest (>= 6.2.3) <!nocheck>,
python3-pytest-asyncio <!nocheck>,
python3-pytest-xdist <!nocheck>,
- python3-rich (>= 10.7.0) <!nocheck>,
+ python3-rich (>= 13.9.4-1.1~) <!nocheck>,
python3-syrupy <!nocheck>,
python3-typing-extensions <!nocheck>,
Standards-Version: 4.6.2
diff -Nru textual-2.1.2/debian/patches/pickle-style-meta.patch
textual-2.1.2/debian/patches/pickle-style-meta.patch
--- textual-2.1.2/debian/patches/pickle-style-meta.patch 1970-01-01
01:00:00.000000000 +0100
+++ textual-2.1.2/debian/patches/pickle-style-meta.patch 2026-01-12
11:18:59.000000000 +0000
@@ -0,0 +1,25 @@
+From: Colin Watson <[email protected]>
+Date: Mon, 12 Jan 2026 11:18:22 +0000
+Subject: Use pickle for style meta information
+
+Backported from https://github.com/Textualize/textual/pull/6169; needed
+to match rich 14.2.0 / 13.9.4-1.1.
+
+Last-Update: 2026-01-12
+---
+ src/textual/style.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/src/textual/style.py b/src/textual/style.py
+index c96c9b7..7df24da 100644
+--- a/src/textual/style.py
++++ b/src/textual/style.py
+@@ -9,7 +9,7 @@ from __future__ import annotations
+
+ from dataclasses import dataclass
+ from functools import cached_property, lru_cache
+-from marshal import dumps, loads
++from pickle import dumps, loads
+ from typing import TYPE_CHECKING, Any, Iterable, Mapping
+
+ import rich.repr
diff -Nru textual-2.1.2/debian/patches/series
textual-2.1.2/debian/patches/series
--- textual-2.1.2/debian/patches/series 1970-01-01 01:00:00.000000000 +0100
+++ textual-2.1.2/debian/patches/series 2026-01-12 11:18:29.000000000 +0000
@@ -0,0 +1 @@
+pickle-style-meta.patch