blob: c4618cc552fcef66d6031a8d12a941628a228b8b [file] [log] [blame]
#!/usr/bin/env python
r"""
This module provides many valuable print functions such as sprint_var,
sprint_time, sprint_error, sprint_call_stack.
"""
import sys
import os
import time
import inspect
import re
import grp
import socket
import argparse
# Setting these variables for use both inside this module and by programs
# importing this module.
pgm_dir_path = sys.argv[0]
pgm_name = os.path.basename(pgm_dir_path)
# Some functions (e.g. sprint_pgm_header) have need of a program name value
# that looks more like a valid variable name. Therefore, we'll swap odd
# characters like "." out for underscores.
pgm_name_var_name = pgm_name.replace(".", "_")
# Initialize global values used as defaults by print_time, print_var, etc.
col1_indent = 0
# Calculate default column width for print_var functions based on environment
# variable settings. The objective is to make the variable values line up
# nicely with the time stamps.
col1_width = 29
if 'NANOSECONDS' in os.environ:
NANOSECONDS = os.environ['NANOSECONDS']
else:
NANOSECONDS = 0
if NANOSECONDS == "1":
col1_width = col1_width + 7
if 'SHOW_ELAPSED_TIME' in os.environ:
SHOW_ELAPSED_TIME = os.environ['SHOW_ELAPSED_TIME']
else:
SHOW_ELAPSED_TIME = 0
if SHOW_ELAPSED_TIME == "1":
if NANOSECONDS == "1":
col1_width = col1_width + 14
else:
col1_width = col1_width + 7
# Initialize some time variables used in module functions.
start_time = time.time()
sprint_time_last_seconds = start_time
###############################################################################
def sprint_func_name(stack_frame_ix=None):
r"""
Return the function name associated with the indicated stack frame.
Description of arguments:
stack_frame_ix The index of the stack frame whose
function name should be returned. If the
caller does not specifiy a value, this
function will set the value to 1 which is
the index of the caller's stack frame. If
the caller is the wrapper function
"print_func_name", this function will bump
it up by 1.
"""
# If user specified no stack_frame_ix, we'll set it to a proper default
# value.
if stack_frame_ix is None:
func_name = sys._getframe().f_code.co_name
caller_func_name = sys._getframe(1).f_code.co_name
if func_name[1:] == caller_func_name:
stack_frame_ix = 2
else:
stack_frame_ix = 1
func_name = sys._getframe(stack_frame_ix).f_code.co_name
return func_name
###############################################################################
# get_arg_name is not a print function per se. I have included it in this
# module because it is used by sprint_var which is found in this module.
###############################################################################
def get_arg_name(var,
arg_num=1,
stack_frame_ix=1):
r"""
Return the "name" of an argument passed to a function. This could be a
literal or a variable name.
Description of arguements:
var The variable whose name you want returned.
arg_num The arg number (1 through n) whose name
you wish to have returned. This value
should not exceed the number of arguments
allowed by the target function.
stack_frame_ix The stack frame index of the target
function. This value must be 1 or
greater. 1 would indicate get_arg_name's
stack frame. 2 would be the caller of
get_arg_name's stack frame, etc.
Example 1:
my_var = "mike"
var_name = get_arg_name(my_var)
In this example, var_name will receive the value "my_var".
Example 2:
def test1(var):
# Getting the var name of the first arg to this function, test1.
# Note, in this case, it doesn't matter what you pass as the first arg
# to get_arg_name since it is the caller's variable name that matters.
dummy = 1
arg_num = 1
stack_frame = 2
var_name = get_arg_name(dummy, arg_num, stack_frame)
# Mainline...
another_var = "whatever"
test1(another_var)
In this example, var_name will be set to "another_var".
"""
# Note: I wish to avoid recursion so I refrain from calling any function
# that calls this function (i.e. sprint_var, valid_value, etc.).
try:
# The user can set environment variable "GET_ARG_NAME_DEBUG" to get
# debug output from this function.
local_debug = os.environ['GET_ARG_NAME_DEBUG']
except KeyError:
local_debug = 0
if arg_num < 1:
print_error("Programmer error - Variable \"arg_num\" has an invalid" +
" value of \"" + str(arg_num) + "\". The value must be" +
" an integer that is greater than 0.\n")
# What is the best way to handle errors? Raise exception? I'll
# revisit later.
return
if stack_frame_ix < 1:
print_error("Programmer error - Variable \"stack_frame_ix\" has an" +
" invalid value of \"" + str(stack_frame_ix) + "\". The" +
" value must be an integer that is greater than or equal" +
" to 1.\n")
return
if local_debug:
debug_indent = 2
print(sprint_func_name() + "() parms:")
print_varx("var", var, 0, debug_indent)
print_varx("arg_num", arg_num, 0, debug_indent)
print_varx("stack_frame_ix", stack_frame_ix, 0, debug_indent)
try:
frame, filename, cur_line_no, function_name, lines, index = \
inspect.stack()[stack_frame_ix]
except IndexError:
print_error("Programmer error - The caller has asked for information" +
" about the stack frame at index \"" +
str(stack_frame_ix) + "\". However, the stack only" +
" contains " + str(len(inspect.stack())) + " entries." +
" Therefore the stack frame index is out of range.\n")
return
if local_debug:
print("\nVariables retrieved from inspect.stack() function:")
print_varx("frame", frame, 0, debug_indent)
print_varx("filename", filename, 0, debug_indent)
print_varx("cur_line_no", cur_line_no, 0, debug_indent)
print_varx("function_name", function_name, 0, debug_indent)
print_varx("lines", lines, 0, debug_indent)
print_varx("index", index, 0, debug_indent)
composite_line = lines[0].strip()
called_func_name = sprint_func_name(stack_frame_ix)
# 2016/09/01 Mike Walsh (xzy0065) - I added code to handle pvar alias.
# pvar is an alias for print_var. However, when it is used,
# sprint_func_name() returns the non-alias version, i.e. "print_var".
# Adjusting for that here.
substring = composite_line[0:4]
if substring == "pvar":
called_func_name = "pvar"
arg_list_etc = re.sub(".*" + called_func_name, "", composite_line)
if local_debug:
print_varx("called_func_name", called_func_name, 0, debug_indent)
print_varx("composite_line", composite_line, 0, debug_indent)
print_varx("arg_list_etc", arg_list_etc, 0, debug_indent)
# Parse arg list...
# Initialize...
nest_level = -1
arg_ix = 0
args_arr = [""]
for ix in range(0, len(arg_list_etc)):
char = arg_list_etc[ix]
# Set the nest_level based on whether we've encounted a parenthesis.
if char == "(":
nest_level += 1
if nest_level == 0:
continue
elif char == ")":
nest_level -= 1
if nest_level < 0:
break
# If we reach a comma at base nest level, we are done processing an
# argument so we increment arg_ix and initialize a new args_arr entry.
if char == "," and nest_level == 0:
arg_ix += 1
args_arr.append("")
continue
# For any other character, we append it it to the current arg array
# entry.
args_arr[arg_ix] += char
# Trim whitespace from each list entry.
args_arr = [arg.strip() for arg in args_arr]
if arg_num > len(args_arr):
print_error("Programmer error - The caller has asked for the name of" +
" argument number \"" + str(arg_num) + "\" but there " +
"were only \"" + str(len(args_arr)) + "\" args used:\n" +
sprint_varx("args_arr", args_arr))
return
argument = args_arr[arg_num - 1]
if local_debug:
print_varx("args_arr", args_arr, 0, debug_indent)
print_varx("argument", argument, 0, debug_indent)
return argument
###############################################################################
###############################################################################
def sprint_time(buffer=""):
r"""
Return the time in the following format.
Example:
The following python code...
sys.stdout.write(sprint_time())
sys.stdout.write("Hi.\n")
Will result in the following type of output:
#(CDT) 2016/07/08 15:25:35 - Hi.
Example:
The following python code...
sys.stdout.write(sprint_time("Hi.\n"))
Will result in the following type of output:
#(CDT) 2016/08/03 17:12:05 - Hi.
The following environment variables will affect the formatting as
described:
NANOSECONDS This will cause the time stamps to be
precise to the microsecond (Yes, it
probably should have been named
MICROSECONDS but the convention was set
long ago so we're sticking with it).
Example of the output when environment
variable NANOSECONDS=1.
#(CDT) 2016/08/03 17:16:25.510469 - Hi.
SHOW_ELAPSED_TIME This will cause the elapsed time to be
included in the output. This is the
amount of time that has elapsed since the
last time this function was called. The
precision of the elapsed time field is
also affected by the value of the
NANOSECONDS environment variable. Example
of the output when environment variable
NANOSECONDS=0 and SHOW_ELAPSED_TIME=1.
#(CDT) 2016/08/03 17:17:40 - 0 - Hi.
Example of the output when environment variable NANOSECONDS=1 and
SHOW_ELAPSED_TIME=1.
#(CDT) 2016/08/03 17:18:47.317339 - 0.000046 - Hi.
Description of arguments.
buffer This will be appended to the formatted
time string.
"""
global NANOSECONDS
global SHOW_ELAPSED_TIME
global sprint_time_last_seconds
seconds = time.time()
loc_time = time.localtime(seconds)
nanoseconds = "%0.6f" % seconds
pos = nanoseconds.find(".")
nanoseconds = nanoseconds[pos:]
time_string = time.strftime("#(%Z) %Y/%m/%d %H:%M:%S", loc_time)
if NANOSECONDS == "1":
time_string = time_string + nanoseconds
if SHOW_ELAPSED_TIME == "1":
cur_time_seconds = seconds
math_string = "%9.9f" % cur_time_seconds + " - " + "%9.9f" % \
sprint_time_last_seconds
elapsed_seconds = eval(math_string)
if NANOSECONDS == "1":
elapsed_seconds = "%11.6f" % elapsed_seconds
else:
elapsed_seconds = "%4i" % elapsed_seconds
sprint_time_last_seconds = cur_time_seconds
time_string = time_string + " - " + elapsed_seconds
return time_string + " - " + buffer
###############################################################################
###############################################################################
def sprint_timen(buffer=""):
r"""
Append a line feed to the buffer, pass it to sprint_time and return the
result.
"""
return sprint_time(buffer + "\n")
###############################################################################
###############################################################################
def sprint_error(buffer=""):
r"""
Return a standardized error string. This includes:
- A time stamp
- The "**ERROR**" string
- The caller's buffer string.
Example:
The following python code...
print(sprint_error("Oops.\n"))
Will result in the following type of output:
#(CDT) 2016/08/03 17:12:05 - **ERROR** Oops.
Description of arguments.
buffer This will be appended to the formatted
error string.
"""
return sprint_time() + "**ERROR** " + buffer
###############################################################################
###############################################################################
def sprint_varx(var_name,
var_value,
hex=0,
loc_col1_indent=col1_indent,
loc_col1_width=col1_width):
r"""
Print the var name/value passed to it. If the caller lets loc_col1_width
default, the printing lines up nicely with output generated by the
print_time functions.
Note that the sprint_var function (defined below) can be used to call this
function so that the programmer does not need to pass the var_name.
sprint_var will figure out the var_name. The sprint_var function is the
one that would normally be used by the general user.
For example, the following python code:
first_name = "Mike"
print_time("Doing this...\n")
print_varx("first_name", first_name)
print_time("Doing that...\n")
Will generate output like this:
#(CDT) 2016/08/10 17:34:42.847374 - 0.001285 - Doing this...
first_name: Mike
#(CDT) 2016/08/10 17:34:42.847510 - 0.000136 - Doing that...
This function recognizes several complex types of data such as dict, list
or tuple.
For example, the following python code:
my_dict = dict(one=1, two=2, three=3)
print_var(my_dict)
Will generate the following output:
my_dict:
my_dict[three]: 3
my_dict[two]: 2
my_dict[one]: 1
Description of arguments.
var_name The name of the variable to be printed.
var_value The value of the variable to be printed.
hex This indicates that the value should be
printed in hex format. It is the user's
responsibility to ensure that a var_value
contains a valid hex number.
loc_col1_indent The number of spaces to indent the output.
loc_col1_width The width of the output column containing
the variable name. The default value of
this is adjusted so that the var_value
lines up with text printed via the
print_time function.
"""
# Adjust loc_col1_width.
loc_col1_width = loc_col1_width - loc_col1_indent
# Determine the type
if type(var_value) in (int, float, bool, str, unicode) \
or var_value is None:
# The data type is simple in the sense that it has no subordinate
# parts.
# See if the user wants the output in hex format.
if hex:
value_format = "0x%08x"
else:
value_format = "%s"
format_string = "%" + str(loc_col1_indent) + "s%-" \
+ str(loc_col1_width) + "s" + value_format + "\n"
return format_string % ("", var_name + ":", var_value)
else:
# The data type is complex in the sense that it has subordinate parts.
format_string = "%" + str(loc_col1_indent) + "s%s\n"
buffer = format_string % ("", var_name + ":")
loc_col1_indent += 2
if type(var_value) is dict:
for key, value in var_value.iteritems():
buffer += sprint_varx(var_name + "[" + key + "]", value, hex,
loc_col1_indent)
elif type(var_value) in (list, tuple):
for key, value in enumerate(var_value):
buffer += sprint_varx(var_name + "[" + str(key) + "]", value,
hex, loc_col1_indent)
elif type(var_value) is argparse.Namespace:
for key in var_value.__dict__:
cmd_buf = "buffer += sprint_varx(var_name + \".\" + str(key)" \
+ ", var_value." + key + ", hex, loc_col1_indent)"
exec(cmd_buf)
else:
var_type = type(var_value).__name__
func_name = sys._getframe().f_code.co_name
var_value = "<" + var_type + " type not supported by " \
+ func_name + "()>"
value_format = "%s"
loc_col1_indent -= 2
format_string = "%" + str(loc_col1_indent) + "s%-" \
+ str(loc_col1_width) + "s" + value_format + "\n"
return format_string % ("", var_name + ":", var_value)
return buffer
return ""
###############################################################################
###############################################################################
def sprint_var(*args):
r"""
Figure out the name of the first argument for you and then call
sprint_varx with it. Therefore, the following 2 calls are equivalent:
sprint_varx("var1", var1)
sprint_var(var1)
"""
# Get the name of the first variable passed to this function.
stack_frame = 2
calling_func_name = sprint_func_name(2)
if calling_func_name == "print_var":
stack_frame += 1
var_name = get_arg_name(None, 1, stack_frame)
return sprint_varx(var_name, *args)
###############################################################################
###############################################################################
def sprint_dashes(loc_col1_indent=col1_indent,
col_width=80,
line_feed=1):
r"""
Return a string of dashes to the caller.
Description of arguements:
indent The number of characters to indent the
output.
width The width of the string of dashes.
line_feed Indicates whether the output should end
with a line feed.
"""
col_width = int(col_width)
buffer = " "*int(loc_col1_indent) + "-"*col_width
if line_feed:
buffer += "\n"
return buffer
###############################################################################
###############################################################################
def sprint_call_stack():
r"""
Return a call stack report for the given point in the program with line
numbers, function names and function parameters and arguments.
Sample output:
-------------------------------------------------------------------------
Python function call stack
Line # Function name and arguments
------ ------------------------------------------------------------------
424 sprint_call_stack ()
4 print_call_stack ()
31 func1 (last_name = 'walsh', first_name = 'mikey')
59 /tmp/scr5.py
-------------------------------------------------------------------------
Description of arguments:
indent The number of characters to indent each
line of output.
stack_frame_ix The index of the first stack frame which
is to be returned.
"""
buffer = ""
buffer += sprint_dashes()
buffer += "Python function call stack\n\n"
buffer += "Line # Function name and arguments\n"
buffer += sprint_dashes(0, 6, 0) + " " + sprint_dashes(0, 73)
# Grab the current program stack.
current_stack = inspect.stack()
# Process each frame in turn.
format_string = "%6s %s\n"
for stack_frame in current_stack:
lineno = str(stack_frame[2])
func_name = str(stack_frame[3])
if func_name == "?":
# "?" is the name used when code is not in a function.
func_name = "(none)"
if func_name == "<module>":
# If the func_name is the "main" program, we simply get the command
# line call string.
func_and_args = ' '.join(sys.argv)
else:
# Get the program arguments.
arg_vals = inspect.getargvalues(stack_frame[0])
function_parms = arg_vals[0]
frame_locals = arg_vals[3]
args_arr = []
for arg_name in function_parms:
# Get the arg value from frame locals.
arg_value = frame_locals[arg_name]
args_arr.append(arg_name + " = " + repr(arg_value))
args_str = "(" + ', '.join(map(str, args_arr)) + ")"
# Now we need to print this in a nicely-wrapped way.
func_and_args = func_name + " " + args_str
buffer += format_string % (lineno, func_and_args)
buffer += sprint_dashes()
return buffer
###############################################################################
###############################################################################
def sprint_executing(stack_frame_ix=None):
r"""
Print a line indicating what function is executing and with what parameter
values. This is useful for debugging.
Sample output:
#(CDT) 2016/08/25 17:54:27 - Executing: func1 (x = 1)
Description of arguments:
stack_frame_ix The index of the stack frame whose
function info should be returned. If the
caller does not specifiy a value, this
function will set the value to 1 which is
the index of the caller's stack frame. If
the caller is the wrapper function
"print_executing", this function will bump
it up by 1.
"""
# If user wants default stack_frame_ix.
if stack_frame_ix is None:
func_name = sys._getframe().f_code.co_name
caller_func_name = sys._getframe(1).f_code.co_name
if func_name[1:] == caller_func_name:
stack_frame_ix = 2
else:
stack_frame_ix = 1
stack_frame = inspect.stack()[stack_frame_ix]
func_name = str(stack_frame[3])
if func_name == "?":
# "?" is the name used when code is not in a function.
func_name = "(none)"
if func_name == "<module>":
# If the func_name is the "main" program, we simply get the command
# line call string.
func_and_args = ' '.join(sys.argv)
else:
# Get the program arguments.
arg_vals = inspect.getargvalues(stack_frame[0])
function_parms = arg_vals[0]
frame_locals = arg_vals[3]
args_arr = []
for arg_name in function_parms:
# Get the arg value from frame locals.
arg_value = frame_locals[arg_name]
args_arr.append(arg_name + " = " + repr(arg_value))
args_str = "(" + ', '.join(map(str, args_arr)) + ")"
# Now we need to print this in a nicely-wrapped way.
func_and_args = func_name + " " + args_str
return sprint_time() + "Executing: " + func_and_args + "\n"
###############################################################################
###############################################################################
def sprint_pgm_header():
r"""
Return a standardized header that programs should print at the beginning
of the run. It includes useful information like command line, pid,
userid, program parameters, etc.
"""
buffer = "\n"
buffer += sprint_time() + "Running " + pgm_name + ".\n"
buffer += sprint_time() + "Program parameter values, etc.:\n\n"
buffer += sprint_varx("command_line", ' '.join(sys.argv))
# We want the output to show a customized name for the pid and pgid but we
# want it to look like a valid variable name. Therefore, we'll use
# pgm_name_var_name which was set when this module was imported.
buffer += sprint_varx(pgm_name_var_name + "_pid", os.getpid())
buffer += sprint_varx(pgm_name_var_name + "_pgid", os.getpgrp())
buffer += sprint_varx("uid", str(os.geteuid()) + " (" + os.getlogin() +
")")
buffer += sprint_varx("gid", str(os.getgid()) + " (" +
str(grp.getgrgid(os.getgid()).gr_name) + ")")
buffer += sprint_varx("host_name", socket.gethostname())
buffer += sprint_varx("DISPLAY", os.environ['DISPLAY'])
# I want to add code to print caller's parms.
buffer += "\n"
return buffer
###############################################################################
###############################################################################
def sissuing(cmd_buf):
r"""
Return a line indicating a command that the program is about to execute.
Sample output for a cmd_buf of "ls"
#(CDT) 2016/08/25 17:57:36 - Issuing: ls
Description of args:
cmd_buf The command to be executed by caller.
"""
buffer = sprint_time() + "Issuing: " + cmd_buf + "\n"
return buffer
###############################################################################
###############################################################################
def sprint_pgm_footer():
r"""
Return a standardized footer that programs should print at the end of the
program run. It includes useful information like total run time, etc.
"""
buffer = "\n" + sprint_time() + "Finished running " + pgm_name + ".\n\n"
total_time = time.time() - start_time
total_time_string = "%0.6f" % total_time
buffer += sprint_varx(pgm_name_var_name + "runtime", total_time_string)
return buffer
###############################################################################
###############################################################################
# In the following section of code, we will dynamically create print versions
# for each of the sprint functions defined above. So, for example, where we
# have an sprint_time() function defined above that returns the time to the
# caller in a string, we will create a corresponding print_time() function that
# will print that string directly to stdout.
# It can be complicated to follow what's being creaed by the exec statement
# below. Here is an example of the print_time() function that will be created:
# def print_time(*args):
# s_funcname = "s" + sys._getframe().f_code.co_name
# s_func = getattr(sys.modules[__name__], s_funcname)
# sys.stdout.write(s_func(*args))
# Here are comments describing the 3 lines in the body of the created function.
# Calculate the "s" version of this function name (e.g. if this function name
# is print_time, we want s_funcname to be "sprint_time".
# Put a reference to the "s" version of this function in s_func.
# Call the "s" version of this function passing it all of our arguments. Write
# the result to stdout.
# func_names contains a list of all print functions which should be created
# from their sprint counterparts.
func_names = ['print_time', 'print_timen', 'print_error', 'print_varx',
'print_var', 'print_dashes', 'print_call_stack',
'print_func_name', 'print_executing', 'print_pgm_header',
'issuing', 'print_pgm_footer']
for func_name in func_names:
# Create abbreviated aliases (e.g. spvar is an alias for sprint_var).
alias = re.sub("print_", "p", func_name)
exec("s" + alias + " = s" + func_name)
for func_name in func_names:
if func_name == "print_error":
output_stream = "stderr"
else:
output_stream = "stdout"
func_def = \
[
"def " + func_name + "(*args):",
" s_func_name = \"s\" + sys._getframe().f_code.co_name",
" s_func = getattr(sys.modules[__name__], s_func_name)",
" sys." + output_stream + ".write(s_func(*args))",
" sys." + output_stream + ".flush()"
]
pgm_definition_string = '\n'.join(func_def)
exec(pgm_definition_string)
# Create abbreviated aliases (e.g. pvar is an alias for print_var).
alias = re.sub("print_", "p", func_name)
exec(alias + " = " + func_name)
###############################################################################