Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display path of current source file in header bar #464

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions pudb/debugger.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,8 @@ def _runmodule(self, module_name):
# UI stuff --------------------------------------------------------------------

from pudb.ui_tools import make_hotkey_markup, labelled_value, \
SelectableText, SignalWrap, StackFrame, BreakpointFrame
SelectableText, SignalWrap, StackFrame, BreakpointFrame, \
Caption, CaptionParts

from pudb.var_view import FrameVarInfoKeeper

Expand Down Expand Up @@ -858,7 +859,7 @@ def helpside(w, size, key):
],
dividechars=1)

self.caption = urwid.Text("")
self.caption = Caption(CaptionParts._make([(None, "")]*4))
header = urwid.AttrMap(self.caption, "header")
self.top = SignalWrap(urwid.Frame(
urwid.AttrMap(self.columns, "background"),
Expand Down Expand Up @@ -2617,26 +2618,26 @@ def interaction(self, exc_tuple, show_exc_dialog=True):
self.current_exc_tuple = exc_tuple

from pudb import VERSION
caption = [(None,
"PuDB %s - ?:help n:next s:step into b:breakpoint "
"!:python command line"
% VERSION)]
pudb_version = (None, "PuDB %s" % VERSION)
hotkey = (None, "?:help")
if self.source_code_provider.get_source_identifier():
filename = (None, self.source_code_provider.get_source_identifier())
else:
filename = (None, "source filename is unavailable")
optional_alert = (None, "")

if self.debugger.post_mortem:
if show_exc_dialog and exc_tuple is not None:
self.show_exception_dialog(exc_tuple)

caption.extend([
(None, " "),
("warning", "[POST-MORTEM MODE]")
])
optional_alert = ("warning", "[POST-MORTEM MODE]")

elif exc_tuple is not None:
caption.extend([
(None, " "),
("warning", "[PROCESSING EXCEPTION - hit 'e' to examine]")
])
optional_alert = \
("warning", "[PROCESSING EXCEPTION, hit 'e' to examine]")

self.caption.set_text(caption)
self.caption.set_text(CaptionParts(
pudb_version, hotkey, filename, optional_alert))
self.event_loop()

def set_source_code_provider(self, source_code_provider, force_update=False):
Expand Down
90 changes: 90 additions & 0 deletions pudb/ui_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,4 +332,94 @@ def keypress(self, size, key):

return result


from collections import namedtuple
caption_parts = ["pudb_version", "hotkey", "full_source_filename", "optional_alert"]
CaptionParts = namedtuple(
"CaptionParts",
caption_parts,
)


class Caption(urwid.Text):
"""
A text widget that will automatically shorten its content
to fit in 1 row if needed
"""

def __init__(self, caption_parts, separator=(None, " - ")):
self.separator = separator
super().__init__(caption_parts)

def __str__(self):
caption_text = self.separator[1].join(
[part[1] for part in self.caption_parts]).rstrip(self.separator[1])
return caption_text

@property
def markup(self):
"""
Returns markup of str(self) by inserting the markup of
self.separator between each item in self.caption_parts
"""

# Reference: https://stackoverflow.com/questions/5920643/add-an-item-between-each-item-already-in-the-list # noqa
markup = [self.separator] * (len(self.caption_parts) * 2 - 1)
markup[0::2] = self.caption_parts
if not self.caption_parts.optional_alert[1]:
markup = markup[:-2]
return markup

def render(self, size, focus=False):
markup = self._get_fit_width_markup(size)
return urwid.Text(markup).render(size)

def set_text(self, caption_parts):
markup = [(attr, str(content)) for (attr, content) in caption_parts]
self.caption_parts = CaptionParts._make(markup)
super().set_text(markup)

def rows(self, size, focus=False):
# Always return 1 to avoid
# AssertionError: `assert head.rows() == hrows, "rows, render mismatch")`
# in urwid.Frame.render() in urwid/container.py
return 1

def _get_fit_width_markup(self, size):
if urwid.Text(str(self)).rows(size) == 1:
return self.markup
filename_markup_index = 4
maxcol = size[0]
markup = self.markup
markup[filename_markup_index] = (
markup[filename_markup_index][0],
self._get_shortened_source_filename(size))
caption = urwid.Text(markup)
while True:
if caption.rows(size) == 1:
return markup
else:
for i in range(len(markup)):
clip_amount = len(caption.get_text()[0]) - maxcol
markup[i] = (markup[i][0], markup[i][1][clip_amount:])
caption = urwid.Text(markup)

def _get_shortened_source_filename(self, size):
import os
maxcol = size[0]

occupied_width = len(str(self)) - \
len(self.caption_parts.full_source_filename[1])
available_width = max(0, maxcol - occupied_width)
trim_index = len(
self.caption_parts.full_source_filename[1]) - available_width
filename = self.caption_parts.full_source_filename[1][trim_index:]

if self.caption_parts.full_source_filename[1][trim_index-1] == os.sep:
#filename starts with the full name of a directory or file
return filename
else:
first_path_sep_index = filename.find(os.sep)
filename = filename[first_path_sep_index + 1:]
return filename
# }}}
175 changes: 175 additions & 0 deletions test/test_caption.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from pudb.ui_tools import Caption, CaptionParts
import pytest
import urwid


@pytest.fixture
def text_markups():
from collections import namedtuple
Markups = namedtuple("Markups",
["pudb_version", "hotkey", "full_source_filename",
"alert", "default_separator", "custom_separator"])

pudb_version = (None, "PuDB VERSION")
hotkey = (None, "?:help")
full_source_filename = (None, "/home/foo - bar/baz.py")
alert = ("warning", "[POST-MORTEM MODE]")
default_separator = (None, " - ")
custom_separator = (None, " | ")
return Markups(pudb_version, hotkey, full_source_filename,
alert, default_separator, custom_separator)


@pytest.fixture
def captions(text_markups):
empty = CaptionParts._make([(None, "")]*4)
always_display = [
text_markups.pudb_version, text_markups.hotkey,
text_markups.full_source_filename]
return {"empty": Caption(empty),
"without_alert": Caption(CaptionParts._make(
always_display + [(None, "")])),
"with_alert": Caption(CaptionParts._make(
always_display + [text_markups.alert])),
"custom_separator": Caption(CaptionParts._make(
always_display + [(None, "")]),
separator=text_markups.custom_separator),
}


@pytest.fixture
def term_sizes():
def _term_sizes(caption):
caption_length = len(str(caption))
full_source_filename = caption.caption_parts.full_source_filename[1]
cut_only_filename = (
max(1, caption_length - len(full_source_filename) + 5), )
cut_more_than_filename = (max(1, caption_length
- len(full_source_filename) - len("PuDB VE")), )
return {"wider_than_caption": (caption_length + 1, ),
"equals_caption": (max(1, caption_length), ),
"narrower_than_caption": (max(1, caption_length - 10), ),
"cut_only_filename": cut_only_filename,
"cut_more_than_filename": cut_more_than_filename,
"one_col": (1, ),
"cut_at_path_sep": (max(1, caption_length - 1), ),
"lose_some_dir": (max(1, caption_length - 2), ),
"lose_all_dir": (max(1,
caption_length - len("/home/foo - bar/")), ),
"lose_some_filename_chars": (max(1,
caption_length - len("/home/foo - bar/ba")), ),
"lose_all_source": (max(1,
caption_length - len("/home/foo - bar/baz.py")), ),
}
return _term_sizes


def test_init(captions):
for key in ["empty", "without_alert", "with_alert"]:
assert captions[key].separator == (None, " - ")
assert captions["custom_separator"].separator == (None, " | ")


def test_str(captions):
assert str(captions["empty"]) == ""
assert str(captions["without_alert"]) \
== "PuDB VERSION - ?:help - /home/foo - bar/baz.py"
assert str(captions["with_alert"]) \
== "PuDB VERSION - ?:help - /home/foo - bar/baz.py - [POST-MORTEM MODE]"
assert str(captions["custom_separator"]) \
== "PuDB VERSION | ?:help | /home/foo - bar/baz.py"


def test_markup(captions):
assert captions["empty"].markup \
== [(None, ""), (None, " - "),
(None, ""), (None, " - "),
(None, "")]

assert captions["without_alert"].markup \
== [(None, "PuDB VERSION"), (None, " - "),
(None, "?:help"), (None, " - "),
(None, "/home/foo - bar/baz.py")]

assert captions["with_alert"].markup \
== [(None, "PuDB VERSION"), (None, " - "),
(None, "?:help"), (None, " - "),
(None, "/home/foo - bar/baz.py"), (None, " - "),
("warning", "[POST-MORTEM MODE]")]

assert captions["custom_separator"].markup \
== [(None, "PuDB VERSION"), (None, " | "),
(None, "?:help"), (None, " | "),
(None, "/home/foo - bar/baz.py")]


def test_render(captions, term_sizes):
for caption in captions.values():
for size in term_sizes(caption).values():
got = caption.render(size)
markup = caption._get_fit_width_markup(size)
expected = urwid.Text(markup).render(size)
assert list(expected.content()) == list(got.content())


def test_set_text(captions):
assert captions["empty"].caption_parts == CaptionParts._make([(None, "")]*4)
assert captions["without_alert"].caption_parts \
== CaptionParts(
(None, "PuDB VERSION"),
(None, "?:help"),
(None, "/home/foo - bar/baz.py"),
(None, ""))
assert captions["with_alert"].caption_parts \
== CaptionParts(
(None, "PuDB VERSION"),
(None, "?:help"),
(None, "/home/foo - bar/baz.py"),
("warning", "[POST-MORTEM MODE]"))


def test_rows(captions):
for caption in captions.values():
assert caption.rows(size=(99999, 99999)) == 1
assert caption.rows(size=(80, 24)) == 1
assert caption.rows(size=(1, 1)) == 1


def test_get_fit_width_markup(captions, term_sizes):
# No need to check empty caption because
# len(str(caption)) == 0 always smaller than min terminal column == 1
caption = captions["with_alert"]
sizes = term_sizes(caption)
assert caption._get_fit_width_markup(sizes["equals_caption"]) \
== [(None, "PuDB VERSION"), (None, " - "),
(None, "?:help"), (None, " - "),
(None, "/home/foo - bar/baz.py"), (None, " - "),
("warning", "[POST-MORTEM MODE]")]
assert caption._get_fit_width_markup(sizes["cut_only_filename"]) \
== [(None, "PuDB VERSION"), (None, " - "),
(None, "?:help"), (None, " - "),
(None, "az.py"), (None, " - "), ("warning", "[POST-MORTEM MODE]")]
assert caption._get_fit_width_markup(sizes["cut_more_than_filename"]) \
== [(None, "RSION"), (None, " - "),
(None, "?:help"), (None, " - "),
(None, ""), (None, " - "), ("warning", "[POST-MORTEM MODE]")]
assert caption._get_fit_width_markup(sizes["one_col"]) \
== [(None, "")]*6 + [("warning", "]")]


def test_get_shortened_source_filename(captions, term_sizes):
# No need to check empty caption because
# len(str(caption)) == 0 always smaller than min terminal column == 1
for k in ["with_alert", "without_alert", "custom_separator"]:
sizes = term_sizes(captions[k])
assert captions[k]._get_shortened_source_filename(sizes["cut_at_path_sep"]) \
== "home/foo - bar/baz.py"
assert captions[k]._get_shortened_source_filename(sizes["lose_some_dir"]) \
== "foo - bar/baz.py"
assert captions[k]._get_shortened_source_filename(sizes["lose_all_dir"]) \
== "baz.py"
assert captions[k]._get_shortened_source_filename(
sizes["lose_some_filename_chars"]) \
== "z.py"
assert captions[k]._get_shortened_source_filename(sizes["lose_all_source"]) \
== ""