6
\$\begingroup\$

I have created a wrapper for displaying messages with ArcPy and IDLE (as that is all I have available to myself due to certain circumstances) as I frequently use both to test and develop new tools. However I find myself repeating print statements and arcpy functions to display feedback.

Unfortunately, it has to be in compatible with Python 2.7.

"""REMOVED HEADER"""
import sys
from time import strftime

import arcpy

def display_title(title, char_limit=100):
    # Format the title, and provide a border above and below to seperate the
    # title
    title = ("\n{0}\n{1:^" + str(char_limit) + "}\n{0}\n").format(
        "-" * char_limit, title.upper())

    # Display the title
    print(title)
    arcpy.AddMessage(title)


def display_message(message, char_limit=100):
    # Add the time to the message, and this will also run the
    # beautify_message function
    message = add_time_to_message(message, char_limit)

    # Display the message
    print(message)
    arcpy.AddMessage(message)


def display_warning(message, char_limit=100):
    # Add the time to the message, and this will also run the
    # beautify_message function
    message = add_time_to_message("WARNING: " + message, char_limit)

    # Display the message
    print(message)
    arcpy.AddWarning(message)


def display_error(message, char_limit=100, traceback=False):
    # Add the time to the message, and this will also run the
    # beautify_message function
    message = add_time_to_message("Error" + message, char_limit)

    # Add the Python traceback information (if available)
    if traceback:
        tracebackInfo = traceback.format_exc()
        if tracebackInfo != "None\n":
            message += "\n\nPython Traceback Information:\n"
            message += tracebackInfo

    # Display the message
    print(message)
    arcpy.AddError(message)

    # Terminate the Script now
    sys.exit()


def display_parameters(parameters, char_limit=100):
    # Make sure that the input type is correct
    if isinstance(parameters, tuple) or isinstance(parameters, list):
        if len(parameters) > 1:
            # Then there is more than one, print a list
            var_string = "Parameter list:\n"
            for var in parameters:
                var_string += "{} = {}\n".format(var[0], var[1])
            # Remove the last "\n"
            var_string[:-1]
        else:
            var_string = "{} = {}".format(parameters[0][0], parameters[0][1])

        # Format the message string
        message = add_time_to_message(var_string, char_limit)

        # Display the message
        print(message)
        arcpy.AddMessage(message)


# Format the message so it displays nicely within the progress box
def beautify_message(message, char_limit=100):
    # Create an empty list of beautified lines
    processed_lines = []

    # Split the message into lines that are defined with a new line
    split_message = message.split("\n")

    # Loop through the each split line
    for line in split_message:

        # Set the counter to zero
        counter = 0

        # Loop though each split line to check the length
        while len(line) > char_limit:
            # Then this line is too long, split it up
            split_line = line[counter:(counter + 1) * char_limit]

            # Capture the last space just in case
            space = True if split_line[-1] == " " else 0

            # Split it on spaces
            split_line = split_line.split()

            # Add the chunk to lines
            processed_lines.append(" ".join(split_line[:-1]))

            # Take the left over bit and add it back to line
            line = split_line[-1] + " " * space + line[(
                counter + 1) * char_limit:]

        # Add the last bit to lines
        processed_lines.append(line)

    # Return the final output
    return processed_lines


# Add the timestamp and indent message
def add_time_to_message(message, char_limit=100):
    # Determine the current time in string format to be included in some 
    # messages
    current_time = strftime("%x %X") + " - "

    # Determine the limit for each line if including the time
    limit = char_limit - len(current_time)

    # Break the message up into lines of the right length
    lines = beautify_message(message, limit)

    # Add the time and the first line
    message = current_time + lines[0] + "\n"

    # Add the rest of the lines
    for line in lines[1:]:
        message += " " * len(current_time) + line + "\n"

    # Remove the extra "\n" character
    message = message[:-1]

    # Return the final output
    return message

Any and all constructive criticism welcome!

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Overall, your code is well-formatted and I believe is fairly readable. It clearly accomplishes its job, looks nice! I'll keep this review in python 2.7 since it sounds like your constraints require it, though hopefully you are able to migrate it to 3.x eventually:

Multiple isinstance checks

Instead of:

if isinstance(val, type) or isinstance(val, othertype) or ...

Do

if isinstance(val, (type, othertype, ...))

Guard Clauses

Switch the instance check to a negative one and return early. This removes indentation from your code:

# go from
if isinstance(parameters, (tuple, list)):
    # code

# do
if not isinstance(parameters, (tuple, list)):
    return

# rest of function

It also seems strange that if parameters don't match a type, that a function would silently return rather than raise or at least display an error. I'd reconsider the implementation here:

if not isinstance(parameters, (tuple, list)):
    raise TypeError(
        "Parameters expected to be of type 'list' or 'tuple', got {}"
        .format(type(parameters))
    )

display_parameters

There are a few things to note here:

  1. Do a single concatenation of strings rather than multiple in a loop
  2. Use tuple unpacking where possible
def display_parameters(parameters, char_limit=100):
    if not isinstance(parameters, (tuple, list)):
        raise TypeError(...)

    if len(parameters) > 1:
        var_string = "Parameter list:\n"
    else:
        var_string = ""

    var_string += (
        "\n".join(
                "{} = {}".format(a, b) for a, b, *_ in parameters
        )
    ).rstrip('\n')

    # Format the message string
    message = add_time_to_message(var_string, char_limit)

    # Display the message
    print(message)
    arcpy.AddMessage(message)

beautify_message

This is probably the most confusing function IMO. It's well named, so I understand the intent, but it reads a bit clunky. The counter is really a static 0, since it's never redefined.

Furthermore, in trying to break up lines into n-length chunks, this is the grouper recipe defined in more-itertools (with the options that fit your use-case the best):

from itertools import izip_longest

def grouper(iterable, n, fillvalue=''):
    "Collect data into non-overlapping fixed-length chunks or blocks."
    # grouper('ABCDEFG', 3) → ABC DEF G
    iterators = [iter(iterable)] * n
    return izip_longest(fillvalue=fillvalue, *iterators)

Replacing your while-loop with this code:

# Format the message so it displays nicely within the progress box
def beautify_message(message, char_limit=100):
    processed_lines = []

    for line in message.split('\n'):

        for grp in grouper(line, char_limit):
            processed_lines.append(''.join(grp))

    # Return the final output
    return processed_lines

Docstrings

I would add docstrings to show expected inputs, returns, and general documentation of your code.

\$\endgroup\$

You must log in to answer this question.