Package: src:python-blessed
Version: 1.25-1
User: [email protected]
Usertags: python3.15
Tags: patch, ftbfs, forky, sid

Hi!

While rebuilding the python related packages against the Python 3.15rc1
version we found that python-blessed fails to build from source [1].

The error is: DeprecationWarning: This process (pid=1990) is
multi-threaded, use of forkpty() may lead to deadlocks in the child.

In order to fix it, I had to apply two changes:
- Upstream Commit cee680f: Ignore deprecation warnings fro pty.fork [2]
- Run tests against the tests directory (otherwise a patch in the tests
  tries to run the tests in .pc)

I applied these fixes in the sandbox [3] to be able to build the
packages that depend on python-blessed, please consider applying the
patch to support the upcoming 3.15 version.

Happy hacking,

[1]: https://debusine.debian.net/debian/r-python-python3.15/artifact/4434437/
[2]: 
https://github.com/jquast/blessed/commit/cee680ff7fb3ad31f42ae98582ba74629f1fd6b0
[3]: https://debusine.debian.net/debian/r-python-python3.15/

--
"Can you imagine what I would do if I could do all I can?" -- Sun Tzu
Saludos /\/\ /\ >< `/
diff -Nru python-blessed-1.25/debian/changelog 
python-blessed-1.25/debian/changelog
--- python-blessed-1.25/debian/changelog        2025-12-24 16:26:02.000000000 
+0100
+++ python-blessed-1.25/debian/changelog        2026-09-02 16:19:03.000000000 
+0200
@@ -1,3 +1,12 @@
+python-blessed (1.25-2) UNRELEASED; urgency=medium
+
+  * Team upload.
+  * Add more upstream patch:
+    0002-Update-wrap-logic-and-add-optional-testing-for-Python-3.15.patch
+  * Avoid running tests from .pc
+
+ -- Maximiliano Curia <[email protected]>  Wed, 02 Sep 2026 16:19:03 +0200
+
 python-blessed (1.25-1) unstable; urgency=medium
 
   * Team Upload
diff -Nru 
python-blessed-1.25/debian/patches/0002-Update-wrap-logic-and-add-optional-testing-for-Python-3.15.patch
 
python-blessed-1.25/debian/patches/0002-Update-wrap-logic-and-add-optional-testing-for-Python-3.15.patch
--- 
python-blessed-1.25/debian/patches/0002-Update-wrap-logic-and-add-optional-testing-for-Python-3.15.patch
    1970-01-01 01:00:00.000000000 +0100
+++ 
python-blessed-1.25/debian/patches/0002-Update-wrap-logic-and-add-optional-testing-for-Python-3.15.patch
    2026-09-02 16:19:03.000000000 +0200
@@ -0,0 +1,253 @@
+From: Avram Lubkin <[email protected]>
+Date: Thu, 4 Dec 2025 11:55:11 -0500
+Subject: Update wrap() logic and add optional testing for Python 3.15 (#311)
+
+* Add optional testing for Python 3.15
+
+* Ignore deprecation warnings for pty.fork()
+
+* Update wrap logic to match 3.15
+
+* Add test for placeholder in wrap()
+
+* Ignore deprecation warnings for pty_test()
+Origin: upstream, 
https://github.com/jquast/blessed/commit/cee680ff7fb3ad31f42ae98582ba74629f1fd6b0
+---
+ blessed/sequences.py | 78 +++++++++++++++++++++++++++++++++++++-------
+ tests/accessories.py | 12 ++++---
+ tests/test_wrap.py   | 55 +++++++++++++++++++++++++++++++
+ 3 files changed, 129 insertions(+), 16 deletions(-)
+
+--- a/blessed/sequences.py
++++ b/blessed/sequences.py
+@@ -155,6 +155,7 @@ class SequenceTextWrapper(textwrap.TextW
+         self.term = term
+         textwrap.TextWrapper.__init__(self, width, **kwargs)
+ 
++    # pylint: disable-next=too-complex,too-many-branches
+     def _wrap_chunks(self, chunks):    # type: ignore[no-untyped-def]
+         """
+         Sequence-aware variant of :meth:`textwrap.TextWrapper._wrap_chunks`.
+@@ -174,29 +175,82 @@ class SequenceTextWrapper(textwrap.TextW
+                 f"invalid width {self.width!r}({type(self.width)!r}) (must be 
integer > 0)"
+             )
+ 
++        if self.max_lines is not None:
++            indent = self.subsequent_indent if self.max_lines > 1 else 
self.initial_indent
++            if len(indent) + len(self.placeholder.lstrip()) > self.width:
++                raise ValueError("placeholder too large for max width")
++
+         term = self.term
+-        drop_whitespace = not hasattr(self, 'drop_whitespace'
+-                                      ) or self.drop_whitespace
++
++        # Arrange in reverse order so items can be efficiently popped from a 
stack of chucks.
+         chunks.reverse()
+-        while chunks:
+-            cur_line: List[str] = []
+-            cur_len = 0
++        while chunks:  # pylint: disable=too-many-nested-blocks
++
++            cur_line = []  # Current line.
++            cur_len = 0  # Length of all the chunks in cur_line
++
++            # Figure out which static string will prefix this line.
+             indent = self.subsequent_indent if lines else self.initial_indent
++            # Maximum width for this line.
+             width = self.width - len(indent)
+-            if drop_whitespace and lines and not Sequence(chunks[-1], 
term).strip():
++
++            # First chunk on line is whitespace -- drop it, unless this
++            # is the very beginning of the text (ie. no lines started yet).
++            if self.drop_whitespace and lines and not Sequence(chunks[-1], 
term).strip():
+                 del chunks[-1]
++
+             while chunks:
+                 chunk_len = Sequence(chunks[-1], term).length()
+                 if cur_len + chunk_len > width:
+-                    if chunk_len > width:
+-                        self._handle_long_word(chunks, cur_line, cur_len, 
width)
+                     break
++
+                 cur_line.append(chunks.pop())
+                 cur_len += chunk_len
+-            if drop_whitespace and (cur_line and not Sequence(cur_line[-1], 
term).strip()):
+-                del cur_line[-1]
++
++            # The current line is full, and the next chunk is too big to fit 
on *any* line
++            if chunks and Sequence(chunks[-1], term).length() > width:
++                self._handle_long_word(chunks, cur_line, cur_len, width)
++                cur_len = sum(Sequence(chunk, term).length() for chunk in 
cur_line)
++
++            # If the last chunk on this line is all whitespace, drop it.
++            if self.drop_whitespace and cur_line:
++                chunk = Sequence(cur_line[-1], term)
++                if not chunk.strip():
++                    cur_len -= chunk.length()
++                    del cur_line[-1]
++
+             if cur_line:
+-                lines.append(f'{indent}{"".join(cur_line)}')
++                if (  # pylint: disable=too-many-boolean-expressions
++                    self.max_lines is None
++                    or len(lines) + 1 < self.max_lines
++                    or (
++                        not chunks
++                        or self.drop_whitespace
++                        and len(chunks) == 1
++                        and not chunks[0].strip()
++                    )
++                    and cur_len <= width
++                ):
++                    lines.append(indent + ''.join(cur_line))
++
++                else:
++                    while cur_line:
++                        chunk = Sequence(cur_line[-1], term)
++                        if (chunk.strip() and cur_len + len(self.placeholder) 
<= width):
++                            cur_line.append(self.placeholder)
++                            lines.append(indent + ''.join(cur_line))
++                            break
++                        cur_len -= chunk.length()
++                        del cur_line[-1]
++                    else:
++                        if lines:
++                            prev_line = lines[-1].rstrip()
++                            if len(prev_line) + len(self.placeholder) <= 
self.width:
++                                lines[-1] = prev_line + self.placeholder
++                                break
++                        lines.append(indent + self.placeholder.lstrip())
++                    break
++
+         return lines
+ 
+     def _handle_long_word(self,  # type: ignore[no-untyped-def]
+@@ -223,7 +277,7 @@ class SequenceTextWrapper(textwrap.TextW
+         # If we're allowed to break long words, then do so: put as much
+         # of the next chunk onto the current line as will fit.
+ 
+-        if self.break_long_words:
++        if self.break_long_words and space_left > 0:
+             term = self.term
+             chunk = reversed_chunks[-1]
+             idx = nxt = seq_length = 0
+--- a/tests/accessories.py
++++ b/tests/accessories.py
+@@ -8,6 +8,7 @@ import traceback
+ import contextlib
+ import time
+ import signal
++import warnings
+ 
+ # local
+ from blessed import Terminal
+@@ -92,7 +93,10 @@ class as_subprocess():  # pylint: disabl
+             return
+ 
+         pid_testrunner = os.getpid()
+-        pid, master_fd = pty.fork()  # pylint: 
disable=possibly-used-before-assignment
++        with warnings.catch_warnings():
++            warnings.filterwarnings("ignore", category=DeprecationWarning)
++            pid, master_fd = pty.fork()  # pylint: 
disable=possibly-used-before-assignment
++
+         if pid == self._CHILD_PID:
+             # child process executes function, raises exception
+             # if failed, causing a non-zero exit code, using the
+@@ -321,12 +325,12 @@ def pty_test(child_func, parent_func=Non
+         result = child_func(term)
+         return result.decode('utf-8') if isinstance(result, bytes) else 
(result or '')
+ 
+-    import pty as pty_module  # pylint: disable=import-outside-toplevel
+-
+     if test_name is None:
+         test_name = getattr(child_func, '__name__', 'pty_test')
+ 
+-    pid, master_fd = pty_module.fork()
++    with warnings.catch_warnings():
++        warnings.filterwarnings("ignore", category=DeprecationWarning)
++        pid, master_fd = pty.fork()  # pylint: 
disable=possibly-used-before-assignment
+ 
+     # Set PTY window size in parent before child starts reading
+     if pid != 0:
+--- a/tests/test_wrap.py
++++ b/tests/test_wrap.py
+@@ -1,6 +1,7 @@
+ """Tests for Terminal.wrap()"""
+ 
+ # std imports
++import sys
+ import textwrap
+ 
+ # 3rd party
+@@ -19,6 +20,10 @@ TEXTWRAP_KEYWORD_COMBINATIONS = [
+     {'break_long_words': True, 'drop_whitespace': True, 'subsequent_indent': 
''},
+     {'break_long_words': True, 'drop_whitespace': False, 'subsequent_indent': 
' '},
+     {'break_long_words': True, 'drop_whitespace': True, 'subsequent_indent': 
' '},
++    {
++        'break_long_words': True, 'drop_whitespace': False,
++        'subsequent_indent': '', 'max_lines': 4, 'placeholder': '~',
++    },
+ ]
+ if TEST_QUICK:
+     # test only one feature: everything on
+@@ -71,6 +76,18 @@ def test_SequenceWrapper(many_columns, k
+         my_wrapped = term.wrap(pgraph, width=width, **kwargs)
+         my_wrapped_colored = term.wrap(pgraph_colored, width=width, **kwargs)
+ 
++        # Older versions of textwrap could leave a preceding all whitespace 
line
++        # https://github.com/python/cpython/issues/140627
++        if (
++            kwargs.get('drop_whitespace') and
++            sys.version_info[:2] < (3, 15) and
++            not internal_wrapped[0].strip()
++        ):
++            internal_wrapped = internal_wrapped[1:]
++            # # This also means any subsequent indent got applied to the 
first line
++            if kwargs.get('subsequent_indent'):
++                internal_wrapped[0] = 
internal_wrapped[0][len(kwargs['subsequent_indent']):]
++
+         # ensure we textwrap ascii the same as python
+         assert internal_wrapped == my_wrapped
+ 
+@@ -179,3 +196,41 @@ def test_greedy_join_with_cojoining():
+         assert result == ['ca', 'fe\u0301', '-l', 'at', 'te']
+ 
+     child()
++
++
++def test_placeholder():
++    """ENsure placeholder behavior matches stdlib"""
++
++    @as_subprocess
++    def child():
++        term = TestTerminal()
++        text = 'The quick brown fox jumps over the lazy dog'
++        kwargs = {'width': 1, 'max_lines': 3, 'placeholder': '...'}
++
++        try:
++            textwrap.wrap(text, **kwargs)
++        except Exception as e:  # pylint: disable=broad-exception-caught
++            stdlib_exc = e
++        else:
++            stdlib_exc = None
++
++        with pytest.raises(stdlib_exc.__class__) as exc:
++            term.wrap(text, **kwargs)
++        assert exc.value.args == stdlib_exc.args
++
++        kwargs = {'width': 10, 'max_lines': 3, 'placeholder': '...'}
++        assert term.wrap(text, **kwargs) == textwrap.wrap(text, **kwargs)
++
++        text = '1234567890 1234567890 extra'
++        kwargs = {'width': 10, 'max_lines': 2, 'placeholder': '...'}
++        assert term.wrap(text, **kwargs) == textwrap.wrap(text, **kwargs)
++
++        text = '1234567890 1234567890'
++        kwargs = {'width': 10, 'max_lines': 1, 'placeholder': '...'}
++        assert term.wrap(text, **kwargs) == textwrap.wrap(text, **kwargs)
++
++        text = 'short 1234567890 extra'
++        kwargs = {'width': 10, 'max_lines': 2, 'placeholder': '...'}
++        assert term.wrap(text, **kwargs) == textwrap.wrap(text, **kwargs)
++
++    child()
diff -Nru python-blessed-1.25/debian/patches/series 
python-blessed-1.25/debian/patches/series
--- python-blessed-1.25/debian/patches/series   2025-12-24 15:53:31.000000000 
+0100
+++ python-blessed-1.25/debian/patches/series   2026-09-02 16:19:03.000000000 
+0200
@@ -1 +1,2 @@
 0001-Get-rid-of-sphinxcontrib-manpage-needs-for-now.patch
+0002-Update-wrap-logic-and-add-optional-testing-for-Python-3.15.patch
diff -Nru python-blessed-1.25/debian/rules python-blessed-1.25/debian/rules
--- python-blessed-1.25/debian/rules    2025-12-24 16:26:02.000000000 +0100
+++ python-blessed-1.25/debian/rules    2026-09-02 16:19:03.000000000 +0200
@@ -14,5 +14,5 @@
 
 override_dh_auto_test:
 ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS)))
-       PYBUILD_SYSTEM=custom PYBUILD_TEST_ARGS="{interpreter} -m pytest -v -x 
-rs -k 'not SequenceWrapper'" dh_auto_test
+       PYBUILD_SYSTEM=custom PYBUILD_TEST_ARGS="{interpreter} -m pytest -v -x 
-rs tests" dh_auto_test
 endif
diff -Nru python-blessed-1.25/debian/tests/python3-blessed 
python-blessed-1.25/debian/tests/python3-blessed
--- python-blessed-1.25/debian/tests/python3-blessed    2025-12-24 
16:26:02.000000000 +0100
+++ python-blessed-1.25/debian/tests/python3-blessed    2026-09-02 
16:19:03.000000000 +0200
@@ -7,5 +7,5 @@
 ln -s /usr/lib/python3/dist-packages/blessed blessed
 
 for python3 in $python3_versions; do
-    $python3 -m pytest -v -x -rs -k 'not SequenceWrapper' || exit 1
+    $python3 -m pytest -v -x -rs tests || exit 1
 done

Reply via email to