friendly-traceback / friendly

Friendly-traceback's version used in most situations
https://friendly-traceback.github.io/docs/index.html
MIT License
40 stars 5 forks source link

[ENHANCEMENT] Structure the output of Friendly #49

Open mardukbp opened 1 year ago

mardukbp commented 1 year ago

Is your feature request related to a problem? Please describe. Calling Friendly prints the available commands in a seemingly arbitrary order and using a format that is hard to read.

Describe the solution you'd like explain(), what(), why(), where(), hint() (not necessarily in that order) should be at the top of the list because presumably they are the most frequently used.

In order to improve the readability and to help finding commands that do related things, the commands should be organized in sections. For example: ask a friend and formatting.

Describe alternatives you've considered None

Additional context None

aroberge commented 1 year ago

The order of the functions shown is done in increasing length of the function name. Before I had done this ordering, I found that the display was harder to read.

The available functions differ depending on the environment used; by this, I mean that the choices of

will all result with slightly different sets of functions shown by typing Friendly.

While some grouping of functions might make sense, for now, while friendly/friendly_traceback is in development, it would require too much work to verify that all possible environments are updated whenever a change is made in the availability of the functions shown by typing Friendly in an interactive environment.

mardukbp commented 1 year ago

I see. I do not know how the Friendly class of each environment gets its methods, but I would think that which methods are available in a given environment is an orthogonal concern to which attributes the methods have (e.g. the group they belong to).

Here is my proposal for printing the list of available functions grouped by category:

import inspect
from collections import namedtuple
from itertools import groupby

Group = {
    "ASK": "Ask a friend",
    "FORMAT": "Formatting"  
}

def pretty_print(groups):
    newline = "\n"
    group_sep = 2*newline
    cmd_sep = " - "

    def heading(group):
        return group + newline + "-"*len(group) 

    def commands_list(cmds):
        return newline.join([cmd.name + "()" + cmd_sep + cmd.doc.strip() for cmd in cmds])

    return group_sep.join([heading(group) + newline + commands_list(cmds) for group, cmds in groups])

class Friendly:
    def __repr__(self):
        Command = namedtuple("Command", ["group", "name", "doc"])
        cmds = [ Command(f.group, name, f.__doc__) 
                 for (name, f) in inspect.getmembers(Friendly, predicate=inspect.isroutine) 
                 if hasattr(f, "group")
               ]
        by_group = groupby(sorted(cmds, key=lambda cmd: cmd.group), key=lambda cmd: cmd.group)
        return pretty_print(by_group)

    def what(self):
        """
        What happened?
        """
    what.group = Group["ASK"]

    def why(self):
        """
        Why it happened?
        """
    why.group = Group["ASK"]

    def dark(self):
        """
        Set dark mode
        """
    dark.group = Group["FORMAT"]

    def light(self):
        """
        Set light mode
        """
    light.group = Group["FORMAT"]

Here is the output:

Ask a friend
------------
what() - What happened?
why() - Why it happened?

Formatting
----------
dark() - Set dark mode
light() - Set light mode
aroberge commented 1 year ago

Try the following:

set_lang('es')
Friendly

Also, try typing

history

without parenthesis. You can do the same for any function.

I don't know of any method that allow translations of docstrings, which would be needed for your approach to work.

mardukbp commented 1 year ago

What about this?

import inspect
from collections import namedtuple
from itertools import groupby
from enum import Enum

class Lang(Enum):
    en = "English"
    fr = "Français"

language = Lang.en

Group = {
    "ASK": {
        Lang.en: "Ask a friend",
        Lang.fr: "Demande à un ami"
    },
    "FORMAT": {
        Lang.en: "Formatting",
        Lang.fr: "Présentation"
    }
}

def pretty_print(groups):
    newline = "\n"
    group_sep = 2*newline
    cmd_sep = " - "

    def heading(group):
        return group + newline + "-"*len(group) 

    def commands_list(cmds):
        return newline.join([cmd.name + "()" + cmd_sep + cmd.doc.strip() for cmd in cmds])

    return group_sep.join([heading(group) + newline + commands_list(cmds) for group, cmds in groups])

def get_doc(fun, lang):
    return fun.doc[lang]

class PrettyFriendly:
    def __repr__(self):
        Command = namedtuple("Command", ["group", "name", "doc"])
        cmds = [ Command(f.group[language], name, f.doc[language]) 
                 for (name, f) in inspect.getmembers(PrettyFriendly, predicate=inspect.isroutine) 
                 if hasattr(f, "group") and hasattr(f, "doc")
               ]
        by_group = groupby(sorted(cmds, key=lambda cmd: cmd.group), key=lambda cmd: cmd.group)
        return pretty_print(by_group)

    def set_language(self, lang):
        global language
        language = Lang[lang]

    def what(self):
        ...
    what.group = Group["ASK"]
    what.doc = {
        Lang.en: "Shows the generic meaning of a given exception.",
        Lang.fr: "Indique la signification générique d'une exception donnée."
    }

    def why(self):
        ...
    why.group = Group["ASK"]
    why.doc = {
        Lang.en: 'Shows the likely cause of the exception.',
        Lang.fr: "Indique la cause probable de l'exception."
    }

    def dark(self):
        ...
    dark.group = Group["FORMAT"]
    dark.doc = {
        Lang.en: 'Sets a colour scheme designed for a black background.',
        Lang.fr: 'Définit un schéma de couleurs conçu pour un fond noir.'
    }

    def light(self):
        ...
    light.group = Group["FORMAT"]
    light.doc = {
        Lang.en: 'Sets a colour scheme designed for a white background.',
        Lang.fr: 'Définit un schéma de couleurs conçu pour un fond blanc.'
    }

Example session:

In [1]: from PF import PrettyFriendly

In [2]: Pretty = PrettyFriendly()

In [3]: Pretty
Out[3]:
Ask a friend
------------
what() - Shows the generic meaning of a given exception.
why() - Shows the likely cause of the exception.

Formatting
----------
dark() - Sets a colour scheme designed for a black background.
light() - Sets a colour scheme designed for a white background.

In [4]: Pretty.set_language('fr')

In [5]: Pretty
Out[5]:
Demande à un ami
----------------
what() - Indique la signification générique d'une exception donnée.
why() - Indique la cause probable de l'exception.

Présentation
------------
dark() - Définit un schéma de couleurs conçu pour un fond noir.
light() - Définit un schéma de couleurs conçu pour un fond blanc.
aroberge commented 1 year ago

While something like this approach might work (except for the hard-coded languages), one of the problems I have with this idea is that it would no longer be possible to show all the information in the terminal on a single screen, without having to do a lot of scrolling. I've tried to keep the available functions/methods of the Friendly object, and it still has all the following:

The following methods of the Friendly object should also be available as functions.

why(): Shows the likely cause of the exception.
www(): Opens a web browser at a useful location.
dark(): Sets a colour scheme designed for a black background.
hint(): Suggestion sometimes added to a friendly traceback.
what(): Shows the generic meaning of a given exception.
light(): Sets a colour scheme designed for a white background.
plain(): Plain formatting, with no colours added.
where(): Shows where an exception was raised.
enable(): Enable friendly's exception hook.
disable(): Disable friendly's exception hook.
explain(): Shows all the information about the last traceback.
history(): Shows a list of recorded traceback messages. You can also use `history.clear()` and `history.pop()`.
get_lang(): Returns the language currently used.
set_lang(): Sets the language to be used.
python_tb(): Shows a normal Python traceback.
set_debug(): Use True (default) or False to enable debugging methods.
set_width(): Sets the output width in some modes.
show_paths(): Shows the paths corresponding to synonyms used.
friendly_tb(): Shows a simplified Python traceback.
get_include(): Returns the current value used for items to include by default.
set_include(): Sets the items to show by default when an exception is raised.
set_formatter(): Sets the formatter to use for display.
set_highlight(): Sets the highlight colors; bg and fg.
set_background(): Sets the background color.

Introducing a "nice formatting", with various sections, would greatly increase the length of this help. Then one has to decide if it is better to show the most common information first, and require a user in a terminal to scroll back, or show it last - which might not be the optimal method when using a Jupyter notebook.

On the other hand, I do understand that this would make it much easier for someone not familiar with friendly/friendly_traceback to figure out what's available.

There's also the issue of determining exactly how to group the various functions/methods. When using a terminal, I can think of:

[Note to self: get_include is probably not needed, and might be removed.]

Then, there are the debugging methods:

[1]: set_debug()

[2]: Friendly
...

Debugging methods (English only).

_get_info(): Returns the content of the traceback items
_get_tb_data(): Return a special traceback object.
_get_statement(): Returns a special Statement object for SyntaxError.
_print_settings(): Prints the saved settings.
_remove_environment(): Deletes an environment from the saved settings; default: current environment.

About the language

friendly/friendlytraceback uses the gettext infrastructure. Strings to be translated are included in the source code inside a function names ``

_("This will be translated")

extracted by a tool inside a ".pot" file. Translators create individual files containing translations of all the extracted strings (more than 600 so far). When a new translation becomes available (the latest one is Tamil), it simply needs to be inserted in the right place in the directory structure. There is no need to go and edit individual files to add some language information the way you have done it. However, I believe that it might be possible to use the gettext approach and simplify your idea.

mardukbp commented 1 year ago

Thank you for the thorough explanation. Yes, of course. Translation is better handled with metaprogramming. It was just a POC ;)

I understand your concerns. What about using a namespace? That is, Friendly stays as it is. And Friendly.basic displays only the documentation for (what, where, why, hint, explain, history?, www?). The initial banner includes the lines "Type Friendly.basic for basic help" and "Type Friendly to list all available functions". That way a new user gets quick access to the most important functions and no big code changes are required :)

aroberge commented 1 year ago

Yes, I might do something like this and/or have Friendly.help() which simply lists the commands available (in a given environment) and suggests something like Friendly.help(command) for more detailed explanation.

I'll look at this when I have a bit more time.