8
\$\begingroup\$

I have this code, which works for the simple tasks I want. But I have questions.

import colorama

TXT = WTXT = RESET = colorama.Style.RESET_ALL
WARN = WARNED = CAUTION = colorama.Fore.YELLOW
FAIL = FAILED = ERROR = STOP = colorama.Fore.RED
SUCCESS = SUCCEED = PASS = GO = colorama.Fore.GREEN

def warn(*txt):
    a = list(txt)
    a[0] = WARN + a[0]
    a[-1] += WTXT
    print(*a)

def fail(*txt):
    a = list(txt)
    a[0] = FAIL + a[0]
    a[-1] += WTXT
    print(*a)

def okay(*txt): # shouldn't be named "pass" since that's a reserved word
    a = list(txt)
    a[0] = PASS + a[0]
    a[-1] += WTXT
    print(*a)

warn("Unfragmented text")
warn("Fragmented", "text")

fail("Unfragmented text")
fail("Fragmented", "text")

okay("Unfragmented text")
okay("Fragmented", "text")

Above, I can print code in yellow, red or green, which isn't a super powerful stub, but it saves time and energy. Even typing print(FAIL + "..." + WTXT) is a nuisance, as I forget the closing WTXT.

My questions are:

  1. Am I horribly misusing *arg? I've never used it before, and so often stuff that just works can backfire in unexpected ways once I try to get it to do more.
  2. I clearly have code that duplicates itself. Ideally I'd like a def colorize(*txt) the three other functions call, but I'd also like a "color=PASS" default parameter. I have a feeling I'm just being scared by a new concept, so I am missing an easy refactoring.
  3. Is it okay to use PASS as a variable, as lower-case pass is a reserved word?

Of course, there may be more I'm overlooking, and I'd love to know that, too. Thanks!

\$\endgroup\$

5 Answers 5

10
\$\begingroup\$

The globals aren't doing much for you, so get rid of them.

Your code will not do the right thing if an argument fails to print: the colour will not be reset. Add a finally to handle this case.

You're too aggressive with your style reset. Just reset the fore colour and nothing else.

Add type hints.

am I horribly misusing *arg?

No, it's fine.

is it okay to use PASS as a variable, as lower-case pass is a reserved word?

Not really. The convention to get around keywords is an underscore suffix, as in pass_.

from typing import Any

import colorama


def print_colour(fore_colour: str, *text: Any) -> None:
    print(fore_colour, end='')
    try:
        print(*text, end='')
    finally:
        print(colorama.Fore.RESET)


def okay(*text: Any) -> None:
    print_colour(colorama.Fore.GREEN, *text)


def warn(*text: Any) -> None:
    print_colour(colorama.Fore.YELLOW, *text)


def fail(*text: Any) -> None:
    print_colour(colorama.Fore.RED, *text)


def test() -> None:
    class BadClass:
        def __str__(self):
            raise ValueError()

    try:
        fail(BadClass())
    except ValueError:
        pass

    print('Should be in default colour')

    warn("Unfragmented text")
    warn("Fragmented", "text")

    fail("Unfragmented text")
    fail("Fragmented", "text")

    okay("Unfragmented text")
    okay("Fragmented", "text")


if __name__ == '__main__':
    test()
\$\endgroup\$
0
6
\$\begingroup\$

am I horribly misusing *arg? I've never used it before, and so often stuff that just works can backfire in unexpected ways once I try to get it to do more.

I wouldn't say you are misusing it (though my preference would be to make the caller provide a sequence in every situation), but you are overusing it. Every one of your functions assumes it will receive at least one argument, so you should formalize that with a required first parameter.

For example,

def warn(first, *txt):
    a = list(txt)
    first = WARN + first
    a[-1] += WTXT
    print(first, *a)
\$\endgroup\$
4
\$\begingroup\$

Here is my go at it.

First your code doesn't work if the function receives a sequence instead of unpacked strs:

In [339]: okay(['Good'])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[339], line 1
----> 1 okay(['Good'])

Cell In[320], line 15, in okay(*txt)
     13 def okay(*txt): # shouldn't be named "pass" since that's a reserved word
     14     a = list(txt)
---> 15     a[0] = PASS + a[0]
     16     a[-1] += WTXT
     17     print(*a)

TypeError: can only concatenate str (not "list") to str

This is very easy to fix, because if a single list is passed to a function with *args argument(s?), args will be a one-element tuple containing that list:

In [340]: def show_args(*args):
     ...:     return args

In [341]: show_args(['Good'])
Out[341]: (['Good'],)

So we need to check if args contains only one element, and assign the only element to args if it also happens not to be an instance of str.

That said, you don't need the global variables, and there is also no use of having multiple functions that differ in only one variable. You just need to pass them as arguments.

Passing them as Enum will get rid of if statements that get the corresponding style, and also type-hinting what the values should be.

Because of the way Python handles function arguments you need to pass positional arguments before starred arguments.

And finally you don't need to do all that just to print unpacked lists.

Just do this: print('start', *('a', 'b', 'c'), 'end')


Suggested code

import colorama
import contextlib
from enum import Enum
from typing import Any


class MsgType(Enum):
    fail = colorama.Fore.RED
    warn = colorama.Fore.YELLOW
    okay = colorama.Fore.GREEN


def print_message(msgtype: MsgType, *text: Any) -> None:
    if len(text) == 1 and not isinstance((sequence := text[0]), str):
        text = sequence
    with contextlib.suppress(Exception):
        print(msgtype.value, *text, colorama.Style.RESET_ALL)

\$\endgroup\$
3
  • 5
    \$\begingroup\$ Read the docs on suppress: As with any other mechanism that completely suppresses exceptions, this context manager should be used only to cover very specific errors where silently continuing with program execution is known to be the right thing to do. \$\endgroup\$
    – Reinderien
    May 23, 2023 at 14:15
  • 3
    \$\begingroup\$ In other words, suppression is not the correct thing to do here. \$\endgroup\$
    – Reinderien
    May 23, 2023 at 14:15
  • 1
    \$\begingroup\$ Your function adds a leading space to the output (and also a trailing one, but that's probably less of a problem). \$\endgroup\$
    – Schmuddi
    May 24, 2023 at 12:50
1
\$\begingroup\$

Because of the two lines of code a[0] = PASS + a[0] and a[-1] += WTXT, you're artificially restricting the first and last element of a to be strings, when every other element of a can be any object that can be converted to string.

>>> okay('hello', 'how', 'are', 'you')
hello how are you
>>> okay('hello', 4, 2, 'you')
hello 4 2 you
>>> okay(5, 'how', 'are', 'you')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in okay
TypeError: can only concatenate str (not "int") to str
>>> okay('hello', 'how', 'are', 5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in okay
TypeError: unsupported operand type(s) for +=: 'int' and 'str'

You could fix that in several different ways.

One way is to call str on a[0] and a[-1] explicitly:

def okay(*txt): # shouldn't be named "pass" since that's a reserved word
    a = list(txt)
    a[0] = PASS + str(a[0])
    a[-1] = str(a[-1]) + WTXT
    print(*a)

Another way is to pass PASS and WTXT directly to print:

def okay(*txt):
    print(PASS, end='')
    print(*txt)
    print(WTXT, end='')
\$\endgroup\$
1
  • 1
    \$\begingroup\$ @Schmuddi Oops, you're right! How about now? \$\endgroup\$
    – Stef
    May 24, 2023 at 14:20
1
\$\begingroup\$

Any time I encounter an operation that needs to be preceded by something and followed by something else, I give passing consideration to creating a context manager for the purpose.

import colorama


class ColorContextManager:
    def __init__(self, begin_color):
        self.begin_color = begin_color
        self.end_color = colorama.Style.RESET_ALL

    def __enter__(self):
        print(self.begin_color, end='')

    def __exit__(self, *exc_details):
        print(self.end_color, end='')


with ColorContextManager(colorama.Fore.GREEN):
    print('Should be green')

print('Normal 1')

with ColorContextManager(colorama.Fore.YELLOW):
    print('Should be yellow')

print('Normal 2')

with ColorContextManager(colorama.Fore.RED):
    print('Should be red')

print('Normal 3')

Each time one of the with statements is encountered, the context manager's __enter__() method is called to print the start color. When the with block ends (even if it ends because an exception occurred!) the __exit__() method is called to print the end color.

This might take some tweaking to control the spurious newlines caused by multiple print() calls, but it may very well be a clean way to achieve this goal.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.