Skip to content

Commit 025e76b

Browse files
committed
Redesign class Caption(urwid.Text) and fix bug
- Caption is now composed of a CaptionParts namedtuple and a separator, both will be passed to Caption explicitly - separator and parts in CaptionParts will be tuples of the form (attribute, text), similar to text markup in urwid - Caption no longer overestimates the amount of text to be removed.
1 parent 7035e96 commit 025e76b

File tree

3 files changed

+250
-65
lines changed

3 files changed

+250
-65
lines changed

pudb/debugger.py

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,8 @@ def _runmodule(self, module_name):
529529
# UI stuff --------------------------------------------------------------------
530530

531531
from pudb.ui_tools import make_hotkey_markup, labelled_value, \
532-
SelectableText, SignalWrap, StackFrame, BreakpointFrame
532+
SelectableText, SignalWrap, StackFrame, BreakpointFrame, \
533+
Caption, CaptionParts
533534

534535
from pudb.var_view import FrameVarInfoKeeper
535536

@@ -858,8 +859,7 @@ def helpside(w, size, key):
858859
],
859860
dividechars=1)
860861

861-
from pudb.ui_tools import Caption
862-
self.caption = Caption("")
862+
self.caption = Caption(CaptionParts(*[(None, "")]*4))
863863
header = urwid.AttrMap(self.caption, "header")
864864
self.top = SignalWrap(urwid.Frame(
865865
urwid.AttrMap(self.columns, "background"),
@@ -2618,35 +2618,26 @@ def interaction(self, exc_tuple, show_exc_dialog=True):
26182618
self.current_exc_tuple = exc_tuple
26192619

26202620
from pudb import VERSION
2621-
separator = " - "
2622-
pudb_version = "PuDB %s" % VERSION
2623-
hotkey = "?:help"
2621+
pudb_version = (None, "PuDB %s" % VERSION)
2622+
hotkey = (None, "?:help")
26242623
if self.source_code_provider.get_source_identifier():
2625-
source_filename = self.source_code_provider.get_source_identifier()
2624+
filename = (None, self.source_code_provider.get_source_identifier())
26262625
else:
2627-
source_filename = "source filename is unavailable"
2628-
caption = [(None, pudb_version),
2629-
(None, separator),
2630-
(None, hotkey),
2631-
(None, separator),
2632-
(None, source_filename)
2633-
]
2626+
filename = (None, "source filename is unavailable")
2627+
optional_alert = (None, "")
26342628

26352629
if self.debugger.post_mortem:
26362630
if show_exc_dialog and exc_tuple is not None:
26372631
self.show_exception_dialog(exc_tuple)
26382632

2639-
caption.extend([
2640-
(None, separator),
2641-
("warning", "[POST-MORTEM MODE]")
2642-
])
2633+
optional_alert = ("warning", "[POST-MORTEM MODE]")
2634+
26432635
elif exc_tuple is not None:
2644-
caption.extend([
2645-
(None, separator),
2646-
("warning", "[PROCESSING EXCEPTION, hit 'e' to examine]")
2647-
])
2636+
optional_alert = \
2637+
("warning", "[PROCESSING EXCEPTION, hit 'e' to examine]")
26482638

2649-
self.caption.set_text(caption)
2639+
self.caption.set_text(CaptionParts(
2640+
pudb_version, hotkey, filename, optional_alert))
26502641
self.event_loop()
26512642

26522643
def set_source_code_provider(self, source_code_provider, force_update=False):

pudb/ui_tools.py

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -333,60 +333,93 @@ def keypress(self, size, key):
333333
return result
334334

335335

336+
from collections import namedtuple
337+
caption_parts = ["pudb_version", "hotkey", "full_source_filename", "optional_alert"]
338+
CaptionParts = namedtuple(
339+
"CaptionParts",
340+
caption_parts,
341+
defaults=[(None, "")],
342+
)
343+
344+
336345
class Caption(urwid.Text):
337-
def __init__(self, markup, separator=" - "):
346+
"""
347+
A text widget that will automatically shorten its content
348+
to fit in 1 row if needed
349+
"""
350+
351+
def __init__(self, caption_parts, separator=(None, " - ")):
338352
self.separator = separator
339-
super().__init__(markup)
340-
341-
def set_text(self, markup):
342-
super().set_text(markup)
343-
if len(markup) > 0:
344-
# Assume the format of caption is:
345-
# <PuDB version> <hotkey> <source filename> [optional_alert]
346-
caption, _ = self.get_text()
347-
caption_elements = caption.split(self.separator)
348-
self.pudb_version = caption_elements[0]
349-
self.hotkey = caption_elements[1]
350-
self.full_source_filename = caption_elements[2]
351-
self.optional_alert = caption_elements[3] if len(
352-
caption_elements) > 3 else ""
353-
else:
354-
self.pudb_version = self.hotkey = ""
355-
self.full_source_filename = self.optional_alert = ""
353+
super().__init__(caption_parts)
354+
355+
def __str__(self):
356+
caption_text = self.separator[1].join(
357+
[part[1] for part in self.caption_parts]).rstrip(self.separator[1])
358+
return caption_text
359+
360+
@property
361+
def markup(self):
362+
"""
363+
Returns markup of str(self) by inserting the markup of
364+
self.separator between each item in self.caption_parts
365+
"""
366+
367+
# Reference: https://stackoverflow.com/questions/5920643/add-an-item-between-each-item-already-in-the-list # noqa
368+
markup = [self.separator] * (len(self.caption_parts) * 2 - 1)
369+
markup[0::2] = self.caption_parts
370+
if not self.caption_parts.optional_alert[1]:
371+
markup = markup[:-2]
372+
return markup
373+
374+
def render(self, size, focus=False):
375+
markup = self._get_fit_width_markup(size)
376+
return urwid.Text(markup).render(size)
377+
378+
def set_text(self, caption_parts):
379+
super().set_text([*caption_parts])
380+
self.caption_parts = caption_parts
356381

357382
def rows(self, size, focus=False):
358383
# Always return 1 to avoid
359-
# `assert head.rows() == hrows, "rows, render mismatch")`
384+
# AssertionError: `assert head.rows() == hrows, "rows, render mismatch")`
360385
# in urwid.Frame.render() in urwid/container.py
361386
return 1
362387

363-
def render(self, size, focus=False):
388+
def _get_fit_width_markup(self, size):
389+
if urwid.Text(str(self)).rows(size) == 1:
390+
return self.markup
391+
FILENAME_MARKUP_INDEX = 4
364392
maxcol = size[0]
365-
if super().rows(size) > 1:
366-
filename = self.get_shortened_source_filename(size)
367-
else:
368-
filename = self.full_source_filename
369-
caption = self.separator.join(
370-
[self.pudb_version, self.hotkey, filename, self.optional_alert]
371-
).strip(self.separator)
372-
if self.optional_alert:
373-
attr = [("warning", len(caption))]
374-
else:
375-
attr = [(None, 0)]
376-
377-
return make_canvas([caption], [attr], maxcol)
393+
markup = self.markup
394+
markup[FILENAME_MARKUP_INDEX] = (
395+
markup[FILENAME_MARKUP_INDEX][0],
396+
self._get_shortened_source_filename(size))
397+
caption = urwid.Text(markup)
398+
while True:
399+
if caption.rows(size) == 1:
400+
return markup
401+
else:
402+
for i in range(len(markup)):
403+
clip_amount = len(caption.get_text()[0]) - maxcol
404+
markup[i] = (markup[i][0], markup[i][1][clip_amount:])
405+
caption = urwid.Text(markup)
378406

379-
def get_shortened_source_filename(self, size):
407+
def _get_shortened_source_filename(self, size):
380408
import os
381409
maxcol = size[0]
382410

383-
occupied_width = (len(self.pudb_version) + len(self.hotkey)
384-
+ len(self.optional_alert) + len(self.separator)*3)
385-
available_width = maxcol - occupied_width
386-
trim_index = len(self.full_source_filename) - available_width
387-
filename = self.full_source_filename[trim_index:]
388-
first_dirname_index = filename.find(os.sep)
389-
filename = filename[first_dirname_index + 1:]
411+
occupied_width = len(str(self)) - \
412+
len(self.caption_parts.full_source_filename[1])
413+
available_width = max(0, maxcol - occupied_width)
414+
trim_index = len(
415+
self.caption_parts.full_source_filename[1]) - available_width
416+
filename = self.caption_parts.full_source_filename[1][trim_index:]
390417

391-
return filename
418+
if self.caption_parts.full_source_filename[1][trim_index-1] == os.sep:
419+
#filename starts with the full name of a directory or file
420+
return filename
421+
else:
422+
first_path_sep_index = filename.find(os.sep)
423+
filename = filename[first_path_sep_index + 1:]
424+
return filename
392425
# }}}

test/test_caption.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from pudb.ui_tools import Caption, CaptionParts
2+
import pytest
3+
import urwid
4+
5+
6+
@pytest.fixture
7+
def text_markups():
8+
from collections import namedtuple
9+
Markups = namedtuple("Markups",
10+
["pudb_version", "hotkey", "full_source_filename",
11+
"alert", "default_separator", "custom_separator"])
12+
13+
pudb_version = (None, "PuDB VERSION")
14+
hotkey = (None, "?:help")
15+
full_source_filename = (None, "/home/foo - bar/baz.py")
16+
alert = ("warning", "[POST-MORTEM MODE]")
17+
default_separator = (None, " - ")
18+
custom_separator = (None, " | ")
19+
return Markups(pudb_version, hotkey, full_source_filename,
20+
alert, default_separator, custom_separator)
21+
22+
23+
@pytest.fixture
24+
def captions(text_markups):
25+
empty = CaptionParts(*[(None, "")]*4)
26+
always_display = [
27+
text_markups.pudb_version, text_markups.hotkey, text_markups.full_source_filename]
28+
return {"empty": Caption(empty),
29+
"without_alert": Caption(CaptionParts(*always_display)),
30+
"with_alert": Caption(CaptionParts(*always_display, text_markups.alert)),
31+
"custom_separator": Caption(CaptionParts(*always_display),
32+
separator=text_markups.custom_separator),
33+
}
34+
35+
36+
def test_init(captions):
37+
for key in ["empty", "without_alert", "with_alert"]:
38+
assert captions[key].separator == (None, " - ")
39+
assert captions["custom_separator"].separator == (None, " | ")
40+
41+
42+
def test_str(captions):
43+
assert str(captions["empty"]) == ""
44+
assert str(captions["without_alert"]
45+
) == "PuDB VERSION - ?:help - /home/foo - bar/baz.py"
46+
assert str(captions["with_alert"]
47+
) == "PuDB VERSION - ?:help - /home/foo - bar/baz.py - [POST-MORTEM MODE]" # noqa
48+
assert str(captions["custom_separator"]
49+
) == "PuDB VERSION | ?:help | /home/foo - bar/baz.py"
50+
51+
52+
def test_markup(captions):
53+
assert captions["empty"].markup \
54+
== [(None, ""), (None, " - "),
55+
(None, ""), (None, " - "),
56+
(None, "")]
57+
58+
assert captions["without_alert"].markup \
59+
== [(None, "PuDB VERSION"), (None, " - "),
60+
(None, "?:help"), (None, " - "),
61+
(None, "/home/foo - bar/baz.py")]
62+
63+
assert captions["with_alert"].markup \
64+
== [(None, "PuDB VERSION"), (None, " - "),
65+
(None, "?:help"), (None, " - "),
66+
(None, "/home/foo - bar/baz.py"), (None, " - "),
67+
("warning", "[POST-MORTEM MODE]")]
68+
69+
assert captions["custom_separator"].markup \
70+
== [(None, "PuDB VERSION"), (None, " | "),
71+
(None, "?:help"), (None, " | "),
72+
(None, "/home/foo - bar/baz.py")]
73+
74+
75+
def test_render(captions):
76+
for k in captions.keys():
77+
sizes = {"wider_than_caption": (max(1, len(str(captions[k])) + 1), ),
78+
"equals_caption": (max(1, len(str(captions[k]))), ),
79+
"narrower_than_caption": (max(1, len(str(captions[k])) - 10), ),
80+
}
81+
for s in sizes:
82+
got = captions[k].render(sizes[s])
83+
markup = captions[k]._get_fit_width_markup(sizes[s])
84+
expected = urwid.Text(markup).render(sizes[s])
85+
assert list(expected.content()) == list(got.content())
86+
87+
88+
def test_set_text(captions):
89+
assert captions["empty"].caption_parts == CaptionParts(*[(None, "")]*4)
90+
for key in ["without_alert", "custom_separator"]:
91+
assert captions[key].caption_parts \
92+
== CaptionParts(
93+
(None, "PuDB VERSION"),
94+
(None, "?:help"),
95+
(None, "/home/foo - bar/baz.py"))
96+
assert captions["with_alert"].caption_parts \
97+
== CaptionParts(
98+
(None, "PuDB VERSION"),
99+
(None, "?:help"),
100+
(None, "/home/foo - bar/baz.py"),
101+
("warning", "[POST-MORTEM MODE]"))
102+
103+
104+
def test_rows(captions):
105+
for caption in captions.values():
106+
assert caption.rows(size=(99999, 99999)) == 1
107+
assert caption.rows(size=(80, 24)) == 1
108+
assert caption.rows(size=(1, 1)) == 1
109+
110+
111+
def test_get_fit_width_markup(captions):
112+
# No need to check empty caption because
113+
# len(str(caption)) == 0 always smaller than min terminal column == 1
114+
115+
# Set up
116+
caption = captions["with_alert"]
117+
caption_length = len(str(caption))
118+
full_source_filename = caption.caption_parts.full_source_filename[1]
119+
cut_only_filename = (
120+
max(1, caption_length - len(full_source_filename) + 5), )
121+
cut_more_than_filename = (max(1, caption_length
122+
- len(full_source_filename) - len("PuDB VE")), )
123+
sizes = {"cut_only_filename": cut_only_filename,
124+
"cut_more_than_filename": cut_more_than_filename,
125+
"one_col": (1, ),
126+
}
127+
# Test
128+
assert caption._get_fit_width_markup(sizes["cut_only_filename"]) \
129+
== [(None, "PuDB VERSION"), (None, " - "),
130+
(None, "?:help"), (None, " - "),
131+
(None, "az.py"), (None, " - "), ("warning", "[POST-MORTEM MODE]")]
132+
assert caption._get_fit_width_markup(sizes["cut_more_than_filename"]) \
133+
== [(None, "RSION"), (None, " - "),
134+
(None, "?:help"), (None, " - "),
135+
(None, ""), (None, " - "), ("warning", "[POST-MORTEM MODE]")]
136+
assert caption._get_fit_width_markup(sizes["one_col"]) \
137+
== [(None, "")]*6 + [("warning", "]")]
138+
139+
140+
def test_get_shortened_source_filename(captions):
141+
# No need to check empty caption because
142+
# len(str(caption)) == 0 always smaller than min terminal column == 1
143+
for k in ["with_alert", "without_alert", "custom_separator"]:
144+
caption_length = len(str(captions[k]))
145+
sizes = {"cut_at_path_sep": (max(1, caption_length - 1), ),
146+
"lose_some_dir": (max(1, caption_length - 2), ),
147+
"lose_all_dir": (max(1,
148+
caption_length - len("/home/foo - bar/")), ),
149+
"lose_some_filename_chars": (max(1,
150+
caption_length - len("/home/foo - bar/ba")), ),
151+
"lose_all": (max(1, caption_length - len("/home/foo - bar/baz.py")), ),
152+
}
153+
assert captions[k]._get_shortened_source_filename(sizes["cut_at_path_sep"]) \
154+
== "home/foo - bar/baz.py"
155+
assert captions[k]._get_shortened_source_filename(sizes["lose_some_dir"]) \
156+
== "foo - bar/baz.py"
157+
assert captions[k]._get_shortened_source_filename(sizes["lose_all_dir"]) \
158+
== "baz.py"
159+
assert captions[k]._get_shortened_source_filename(
160+
sizes["lose_some_filename_chars"]) == "z.py"
161+
assert captions[k]._get_shortened_source_filename(sizes["lose_all"]) == ""

0 commit comments

Comments
 (0)