"""
console_helpers.py
------------------
Functions that can be used in a friendly console or in other interactive
environments such as in a Jupyter notebook.
"""
# NOTE: __all__ is defined at the very bottom of this file
import sys
import friendly
from friendly import debug_helper, formatters, __version__
from friendly.config import session
from friendly.info_generic import get_generic_explanation
from friendly.path_info import show_paths
from friendly.my_gettext import current_lang
from friendly.utils import add_rich_repr
_ = current_lang.translate
[docs]def back():
"""Removes the last recorded traceback item.
The intention is to allow recovering from a typo when trying interactively
to find out specific information about a given exception.
"""
if not session.saved_info:
session.write_err(_("Nothing to go back to: no exception recorded.") + "\n")
return
if not session.friendly: # pragma: no cover
debug_helper.log("Problem: saved info is not empty but friendly is")
session.saved_info.pop()
session.friendly.pop()
if session.saved_info:
info = session.saved_info[-1]
if info["lang"] != friendly.get_lang():
info["lang"] = friendly.get_lang()
session.friendly[-1].recompile_info()
[docs]def explain(include="explain"):
"""Shows the previously recorded traceback info again,
with the option to specify different items to include.
For example, ``explain("why")`` is equivalent to ``why()``.
"""
old_include = friendly.get_include()
friendly.set_include(include)
session.show_traceback_info_again()
friendly.set_include(old_include)
[docs]def friendly_tb():
"""Shows the a simplified Python traceback,
which includes the hint/suggestion if available.
"""
explain("friendly_tb")
[docs]def hint():
"""Shows hint/suggestion if available."""
explain("hint")
[docs]def history():
"""Prints the list of error messages recorded so far."""
if not session.saved_info:
session.write_err(_("Nothing to show: no exception recorded.") + "\n")
return
session.rich_add_vspace = False
for info in session.saved_info:
message = session.formatter(info, include="message").replace("\n", "")
session.write_err(message)
session.rich_add_vspace = True
[docs]def python_tb():
"""Shows the Python traceback, excluding files from friendly
itself.
"""
explain("python_tb")
[docs]def what(exception=None, pre=False):
"""If known, shows the generic explanation about a given exception.
If the ``pre`` argument is set to ``True``, the output is
formatted in a way that is suitable for inclusion in the
documentation.
"""
if exception is None:
explain("what")
return
if hasattr(exception, "__name__"):
exception = exception.__name__
result = get_generic_explanation(exception)
if pre: # for documentation # pragma: no cover
lines = result.split("\n")
for line in lines:
session.write_err(" " + line + "\n")
session.write_err("\n")
else:
session.write_err(result)
return
[docs]def where():
"""Shows the information about where the exception occurred"""
explain("where")
[docs]def why():
"""Shows the likely cause of the exception."""
explain("why")
[docs]def www(site=None): # pragma: no cover
"""This uses the ``webbrowser`` module to open a tab (or window)
in the default browser, linking to a specific url
or opening the default email client.
* If the argument 'site' is not specified,
and an exception has been raised,
an internet search will be done using
the exception message as the search string.
* If the argument 'site' is not specified,
and NO exception has been raised,
Friendly's documentation will open.
* If the argument 'site' == "friendly",
Friendly's documentation will open.
* If the argument 'site' == "python", Python's documentation site
will open with the currently used Python version.
* If the argument 'site' == "bug",
the Issues page for Friendly on Github will open.
* If the argument 'site' == "email",
the default email client should open with Friendly's
developer's address already filled in.
"""
import urllib
import webbrowser
urls = {
"friendly": "https://aroberge.github.io/friendly-traceback-docs/docs/html/",
"python": "https://docs.python.org/3",
"bug": "https://github.com/aroberge/friendly/issues/new",
"email": "mailto:andre.roberge@gmail.com",
}
try:
site = site.lower()
except Exception: # noqa
pass
if site not in urls and site is not None:
session.write_err(
_(
"Invalid argument for `www()`.\n"
"Valid arguments include `None` or one of `{sites}`.\n"
).format(sites=repr(urls.keys()))
)
return
info = session.saved_info[-1] if session.saved_info else None
if site is None and info is not None:
message = info["message"].replace("'", "")
if " (" in message:
message = message.split("(")[0]
url = "https://duckduckgo.com?q=" + urllib.parse.quote(message) # noqa
elif site is None:
url = urls["friendly"]
else:
url = urls[site]
if site == "python":
url = url + f".{sys.version_info.minor}/"
try:
webbrowser.open_new_tab(url)
except Exception: # noqa
session.write_err(_("The default web browser cannot be used for searching."))
return
get_lang = friendly.get_lang
set_lang = friendly.set_lang
get_include = friendly.get_include
set_include = friendly.set_include
set_formatter = friendly.set_formatter
# ===== Debugging functions are not unit tested by choice =====
def _debug_tb(): # pragma: no cover
"""Shows the true Python traceback, which includes
files from friendly itself.
"""
explain("debug_tb")
def _get_exception(): # pragma: no cover
"""Debugging tool: returns the exception instance or None if no exception
has been raised.
"""
if not session.saved_info:
print("Nothing to show: no exception recorded.")
return
info = session.saved_info[-1]
return info["_exc_instance"]
def _get_frame(): # pragma: no cover
"""This returns the frame in which the exception occurred.
This is not intended for end-users but is useful in development.
"""
if not session.saved_info:
print("Nothing to show: no exception recorded.")
return
info = session.saved_info[-1]
return info["_frame"]
def _get_statement(): # pragma: no cover
"""This returns a 'Statement' instance obtained for SyntaxErrors and
subclasses. Such a Statement instance contains essentially all
the known information about the statement where the error occurred.
This is not intended for end-users but is useful in development.
"""
if not session.saved_info:
print("Nothing to show: no exception recorded.")
return
if isinstance(session.saved_info[-1]["_exc_instance"], SyntaxError):
return session.friendly[-1].tb_data.statement
print("No statement: not a SyntaxError.")
return
def _get_tb_data(): # pragma: no cover
"""This returns the TracebackData instance containing all the
information we have obtained.
This is not intended for end-users but is useful in development.
"""
if not session.saved_info:
print("Nothing to show: no exception recorded.")
return
info = session.saved_info[-1]
return info["_tb_data"]
def _set_debug(flag=True): # pragma: no cover
"""This sets the value of the debug flag for the current session."""
debug_helper.DEBUG = flag
def _show_info(): # pragma: no cover
"""Debugging tool: shows the complete content of traceback info.
Prints ``''`` for a given item if it is not present.
"""
info = session.saved_info[-1] if session.saved_info else []
for item in formatters.items_in_order:
if item in info and info[item].strip():
print(f"{item}:")
for line in info[item].strip().split("\n"):
print(" ", line)
print()
else:
print(f"{item}: ''")
print("=" * 56)
print("The following are not meant to be shown to the end user:\n")
for item in info:
if item not in formatters.items_in_order:
print(f"{item}: {info[item]}")
basic_helpers = {
"back": back,
"explain": explain,
"history": history,
"set_lang": set_lang,
"show_paths": show_paths,
"what": what,
"where": where,
"why": why,
"www": www,
}
back.help = lambda: _("Removes the last recorded traceback item.")
explain.help = lambda: _("Shows all the information about the last traceback.")
history.help = lambda: _("Shows a list of recorded traceback messages.")
set_lang.help = lambda: _("Sets the language to be used.")
show_paths.help = lambda: _("Shows the paths corresponding to synonyms used.")
what.help = lambda: _("Shows the generic meaning of a given exception")
where.help = lambda: _("Shows where an exception was raised.")
why.help = lambda: _("Shows the likely cause of the exception.")
www.help = lambda: _("Opens a web browser at a useful location.")
add_rich_repr(basic_helpers)
other_helpers = {
"hint": hint,
"get_lang": get_lang,
"python_tb": python_tb,
"friendly_tb": friendly_tb,
"get_include": get_include,
"set_include": set_include,
"set_formatter": set_formatter,
}
hint.help = lambda: _("Suggestion sometimes added to a friendly traceback.")
python_tb.help = lambda: _("Shows a normal Python traceback.")
friendly_tb.help = lambda: _("Shows a simplified Python traceback.")
get_include.help = lambda: _(
"Returns the current value used for items to include by default."
)
set_include.help = lambda: _(
"Sets the items to show by default when an exception is raised."
)
get_lang.help = lambda: _("Returns the language currently used.")
set_formatter.help = lambda: _("Sets the formatter to use for display.")
add_rich_repr(other_helpers)
helpers = {**basic_helpers, **other_helpers}
_debug_helpers = {
"_debug_tb": _debug_tb,
"_get_frame": _get_frame,
"_set_debug": _set_debug,
"_show_info": _show_info,
"_get_tb_data": _get_tb_data,
"_get_exception": _get_exception,
"_get_statement": _get_statement,
}
_debug_tb.help = lambda: "Shows the full traceback, including code from friendly."
_show_info.help = lambda: "Shows the all the items recorded in the traceback."
_get_exception.help = lambda: "Returns the exception instance."
_get_frame.help = lambda: "Returns the frame object where the exception occurred."
_get_statement.help = lambda: "Returns the statement in which a SyntaxError occurred."
_get_tb_data.help = lambda: "Return a special traceback object."
_set_debug.help = lambda: "Use True (default) or False to set the debug flag."
class FriendlyHelpers:
"""Helper class which can be used in a console if one of the
helper functions gets redefined.
For example, we can write Friendly.explain() as equivalent to explain().
"""
version = __version__
def __init__(self, local_helpers=None):
self.__include_basic = list(basic_helpers)
if local_helpers is not None:
self.__include_basic.extend(local_helpers)
self.__include_basic = sorted(self.__include_basic) # first alphabetically
# then by word length, as it is easier to read.
self.include_in_rich_repr = sorted(self.__include_basic, key=len)
self.__include_more = list(other_helpers)
self.__include_more = sorted(self.__include_more) # first alphabetically
# then by word length, as it is easier to read.
self.include_in_help = sorted(self.__include_more, key=len)
self.__class__.__name__ = "Friendly" # For a nicer Rich repr
def __dir__(self): # pragma: no cover
"""Only include useful friendly methods."""
return self.__include_basic + list(other_helpers) + list(_debug_helpers)
def __repr__(self): # pragma: no cover
"""Shows a brief description in the default language of what
each 'basic' function/method does.
'Advanced' and debugging helper functions are not included
in the display.
"""
header = (
_(
"The following methods of the Friendly object might also "
"be available as functions."
)
+ "\n\n"
+ _("Basic methods:")
)
parts = [header + "\n\n"]
for item in self.include_in_rich_repr:
parts.append(item + "(): ")
fn = getattr(Friendly, item)
if hasattr(fn, "help"):
parts.append(getattr(Friendly, item).help() + "\n")
else:
print("Warning:", item, "has no help() method.")
more_header = _("Less commonly used methods/functions.")
parts.append("\n" + more_header + "\n\n")
for item in self.include_in_help:
parts.append(item + "(): ")
fn = getattr(Friendly, item)
if hasattr(fn, "help"):
parts.append(getattr(Friendly, item).help() + "\n")
else:
print("Warning:", item, "has no help() method.")
return "".join(parts)
for helper in _debug_helpers:
setattr(FriendlyHelpers, helper, staticmethod(_debug_helpers[helper]))
for helper in helpers:
setattr(FriendlyHelpers, helper, staticmethod(helpers[helper]))
# == Local version; this may need to be removed/modified in
# some programming environments (e.g. Mu, Idle, etc.) which do not support Rich.
class _FriendlyHelpers(FriendlyHelpers): # local version
pass
[docs]def dark(): # pragma: no cover
"""Synonym of set_formatter('dark') designed to be used
within iPython/Jupyter programming environments or at a terminal.
"""
set_formatter("dark")
[docs]def light(): # pragma: no cover
"""Synonym of set_formatter('light') designed to be used
within iPython/Jupyter programming environments or at a terminal.
"""
set_formatter("light")
dark.help = lambda: _("Sets a colour scheme designed for a black background.")
light.help = lambda: _("Sets a colour scheme designed for a white background.")
default_color_schemes = {"dark": dark, "light": light}
add_rich_repr(default_color_schemes)
Friendly = _FriendlyHelpers(default_color_schemes)
for scheme in default_color_schemes:
setattr(_FriendlyHelpers, scheme, staticmethod(default_color_schemes[scheme]))
helpers["Friendly"] = Friendly
__all__ = list(helpers.keys())
__all__.extend(list(default_color_schemes))