Skip to content

Commit d7fd7ca

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 d7fd7ca

File tree

3 files changed

+252
-65
lines changed

3 files changed

+252
-65
lines changed

pudb/debugger.py

+14-23
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

+74-42
Original file line numberDiff line numberDiff line change
@@ -333,60 +333,92 @@ 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+
)
342+
343+
336344
class Caption(urwid.Text):
337-
def __init__(self, markup, separator=" - "):
345+
"""
346+
A text widget that will automatically shorten its content
347+
to fit in 1 row if needed
348+
"""
349+
350+
def __init__(self, caption_parts, separator=(None, " - ")):
338351
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 = ""
352+
super().__init__(caption_parts)
353+
354+
def __str__(self):
355+
caption_text = self.separator[1].join(
356+
[part[1] for part in self.caption_parts]).rstrip(self.separator[1])
357+
return caption_text
358+
359+
@property
360+
def markup(self):
361+
"""
362+
Returns markup of str(self) by inserting the markup of
363+
self.separator between each item in self.caption_parts
364+
"""
365+
366+
# Reference: https://stackoverflow.com/questions/5920643/add-an-item-between-each-item-already-in-the-list # noqa
367+
markup = [self.separator] * (len(self.caption_parts) * 2 - 1)
368+
markup[0::2] = self.caption_parts
369+
if not self.caption_parts.optional_alert[1]:
370+
markup = markup[:-2]
371+
return markup
372+
373+
def render(self, size, focus=False):
374+
markup = self._get_fit_width_markup(size)
375+
return urwid.Text(markup).render(size)
376+
377+
def set_text(self, caption_parts):
378+
super().set_text([*caption_parts])
379+
self.caption_parts = caption_parts
356380

357381
def rows(self, size, focus=False):
358382
# Always return 1 to avoid
359-
# `assert head.rows() == hrows, "rows, render mismatch")`
383+
# AssertionError: `assert head.rows() == hrows, "rows, render mismatch")`
360384
# in urwid.Frame.render() in urwid/container.py
361385
return 1
362386

363-
def render(self, size, focus=False):
387+
def _get_fit_width_markup(self, size):
388+
if urwid.Text(str(self)).rows(size) == 1:
389+
return self.markup
390+
FILENAME_MARKUP_INDEX = 4
364391
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)
392+
markup = self.markup
393+
markup[FILENAME_MARKUP_INDEX] = (
394+
markup[FILENAME_MARKUP_INDEX][0],
395+
self._get_shortened_source_filename(size))
396+
caption = urwid.Text(markup)
397+
while True:
398+
if caption.rows(size) == 1:
399+
return markup
400+
else:
401+
for i in range(len(markup)):
402+
clip_amount = len(caption.get_text()[0]) - maxcol
403+
markup[i] = (markup[i][0], markup[i][1][clip_amount:])
404+
caption = urwid.Text(markup)
378405

379-
def get_shortened_source_filename(self, size):
406+
def _get_shortened_source_filename(self, size):
380407
import os
381408
maxcol = size[0]
382409

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:]
410+
occupied_width = len(str(self)) - \
411+
len(self.caption_parts.full_source_filename[1])
412+
available_width = max(0, maxcol - occupied_width)
413+
trim_index = len(
414+
self.caption_parts.full_source_filename[1]) - available_width
415+
filename = self.caption_parts.full_source_filename[1][trim_index:]
390416

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

test/test_caption.py

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

0 commit comments

Comments
 (0)