cnfunctions.py
A collection of common functions used by CN software
Functions:
| Name | Description |
|---|---|
pascal_case |
Convert a class name to it's Pascal equivalent |
do_bool |
Convert other values, like integers or strings, to bools |
dpretty |
Pretty print a dict |
lpretty |
Pretty print a list |
pp |
Pretty print a dictionary or list |
combine_dicts |
Update a dictionary with one or more dictionaries |
sh |
Run a command string in the shell |
load_dicts |
Combines dictionaries from all found paths |
save_dict |
Save a dictionary to all paths |
CNRunError
Bases: Exception
A class to encapsulate run exceptions
Source code in cnlib/cnfunctions.py
class CNRunError(Exception):
"""
A class to encapsulate run exceptions
"""
# --------------------------------------------------------------------------
# Initialize the class
# --------------------------------------------------------------------------
def __init__(self, cmd, returncode, stdout, stderr, output):
"""
Docstring for __init__
:param self: Description
:param cmd: Description
:param returncode: Description
:param stdout: Description
:param stderr: Description
:param output: Description
"""
# call super __init__
super().__init__()
# set properties from params
self.cmd = cmd
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
self.output = output
# --------------------------------------------------------------------------
# Return a string representation of an instance of the class
# --------------------------------------------------------------------------
def __str__(self):
return (
f"cmd: {self.cmd}, "
f"returncode: {self.returncode}, "
f"stdout: {self.stdout}, "
f"stderr: {self.stderr}, "
f"output: {self.output}"
)
__init__(cmd, returncode, stdout, stderr, output)
Docstring for init
:param self: Description :param cmd: Description :param returncode: Description :param stdout: Description :param stderr: Description :param output: Description
Source code in cnlib/cnfunctions.py
def __init__(self, cmd, returncode, stdout, stderr, output):
"""
Docstring for __init__
:param self: Description
:param cmd: Description
:param returncode: Description
:param stdout: Description
:param stderr: Description
:param output: Description
"""
# call super __init__
super().__init__()
# set properties from params
self.cmd = cmd
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
self.output = output
clamp(val, in_min, in_max)
Constrain a value to an upper or lower value
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
val
|
The value to be converted |
required | |
in_min
|
Lower bound of input range |
required | |
in_max
|
Upper bound of input range |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
if min is greater than max |
Returns:
| Type | Description |
|---|---|
|
The input value constrained to the low and high limits |
Constrain an input value to a min/max value. This will return the input if it falls in the range, or either in_min or in_max if the value is outside that range. The return type (float or int) is the same as the type of the parameter used for the return value, whether it is val, in_min, or in_max.
Source code in cnlib/cnfunctions.py
def clamp(val, in_min, in_max):
"""
Constrain a value to an upper or lower value
Args:
val: The value to be converted
in_min: Lower bound of input range
in_max: Upper bound of input range
Raises:
ValueError: if min is greater than max
Returns:
The input value constrained to the low and high limits
Constrain an input value to a min/max value. This will return the input if
it falls in the range, or either in_min or in_max if the value is outside
that range. The return type (float or int) is the same as the type of the
parameter used for the return value, whether it is val, in_min, or in_max.
"""
# sanity checks
if in_min >= in_max:
raise ValueError("in_min must be less than in_max")
# assume result is input
res = val
# ------------------------------------------------------------------------------
# if val > high, use high
res = min(res, in_max)
# if val < low, use low
res = max(res, in_min)
# ------------------------------------------------------------------------------
# return result
return res
combine_dicts(dicts_new, dict_old=None)
Update a dictionary with entries from another dict
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dicts_new
|
A dictionary or list of dictionaries containing new |
required | |
dict_old
|
The dictionary defined as the one to receive updates |
None
|
|
(default
|
None) |
required |
Returns:
| Type | Description |
|---|---|
|
The updated dict_old, filled with updates from dict_new |
This function takes key/value pairs from each of the new dicts and adds/overwrites these keys and values in dict_old, preserving any values that are blank or None in dict_new. It is also recursive, so a dict or list as a value will be handled correctly. *NOTE: This function DOES NOT test for type mismatches in values for matching keys!!! So if a new dict has a key of "A" and a value of type "str", and the old dict has a key of "A" with a value of type "int", bad things will happen!!!
Source code in cnlib/cnfunctions.py
def combine_dicts(dicts_new, dict_old=None):
"""
Update a dictionary with entries from another dict
Args:
dicts_new: A dictionary or list of dictionaries containing new
keys/values to be updated in the old dictionary
dict_old: The dictionary defined as the one to receive updates
(default: None)
Returns:
The updated dict_old, filled with updates from dict_new
This function takes key/value pairs from each of the new dicts and
adds/overwrites these keys and values in dict_old, preserving any values
that are blank or None in dict_new. It is also recursive, so a dict or list
as a value will be handled correctly.
*NOTE: This function DOES NOT test for type mismatches in values for
matching keys!!!
So if a new dict has a key of "A" and a value of type "str", and the old
dict has a key of "A" with a value of type "int", bad things will
happen!!!
"""
# default return val
if not dict_old:
dict_old = {}
else:
dict_old = dict(dict_old)
# sanity checks
if not isinstance(dicts_new, list):
dicts_new = [dicts_new]
if len(dicts_new) == 0:
return dict_old
# go through the new dicts in order
for dict_new in dicts_new:
# for each k,v pair in dict_new
for k, v in dict_new.items():
# if the value is a dict
if isinstance(v, dict):
# recurse using the current key and value
dict_old[k] = combine_dicts(v, dict_old.get(k, None))
continue
# if the value is not a dict or a list
# just copy value from one dict to the other
dict_old[k] = v
# return the updated dict_old
return dict_old
comp_sem_ver(ver_old, ver_new)
Compare two semantic versions
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ver_old
|
The old version to compare |
required | |
ver_new
|
The new version to compare |
required |
Returns:
| Type | Description |
|---|---|
|
An integer showing the relationship between the two version |
Compare two semantic versions.
Source code in cnlib/cnfunctions.py
def comp_sem_ver(ver_old, ver_new):
"""
Compare two semantic versions
Args:
ver_old: The old version to compare
ver_new: The new version to compare
Returns:
An integer showing the relationship between the two version
Compare two semantic versions.
"""
# sanity checks
if not ver_old or ver_old == "":
return I_VER_ERROR
if not ver_new or ver_new == "":
return I_VER_ERROR
if ver_old == ver_new:
return I_VER_SAME
# --------------------------------------------------------------------------
# compare version string parts (only x.x.x)
res_old = re.search(R_VERSION_VALID, ver_old)
res_new = re.search(R_VERSION_VALID, ver_new)
# if either version string is None
if not res_old or not res_new:
return I_VER_ERROR
# make a list of groups to check
lst_groups = [
R_VERSION_GROUP_MAJ,
R_VERSION_GROUP_MIN,
R_VERSION_GROUP_REV,
]
# for each part as int
for group in lst_groups:
old_val = int(res_old.group(group))
new_val = int(res_new.group(group))
# slide out at the first difference
if old_val < new_val:
return I_VER_NEWER
if old_val > new_val:
return I_VER_OLDER
# --------------------------------------------------------------------------
# still going, check pre
pre_old = res_old.group(R_VERSION_GROUP_PRE)
pre_new = res_new.group(R_VERSION_GROUP_PRE)
# simple pre rule compare
if not pre_old and pre_new:
return I_VER_OLDER
if pre_old and not pre_new:
return I_VER_NEWER
if not pre_old and not pre_new:
return I_VER_SAME
# --------------------------------------------------------------------------
# if pre_old and pre_new:
# split pre on dots
lst_pre_old = pre_old.split(".")
lst_pre_new = pre_new.split(".")
# get number of parts
len_pre_old = len(lst_pre_old)
len_pre_new = len(lst_pre_new)
# get shorter of two
shortest = len_pre_old if len_pre_old <= len_pre_new else len_pre_new
# for each part in shortest
for index in range(shortest):
# get each value at position
old_val = lst_pre_old[index]
new_val = lst_pre_new[index]
# 1. both numbers
if old_val.isdigit() and new_val.isdigit():
tmp_old_val = int(old_val)
tmp_new_val = int(new_val)
# slide out at the first difference
if tmp_old_val > tmp_new_val:
return I_VER_OLDER
if tmp_old_val < tmp_new_val:
return I_VER_NEWER
# 2. both alphanumeric
if not old_val.isdigit() and not new_val.isdigit():
lst_alpha = [old_val, new_val]
lst_alpha.sort()
idx_old = lst_alpha.index(old_val)
idx_new = lst_alpha.index(new_val)
if idx_old > idx_new:
return I_VER_OLDER
if idx_old < idx_new:
return I_VER_NEWER
# 3 num vs alphanumeric
if old_val.isdigit() and not new_val.isdigit():
return I_VER_OLDER
if not old_val.isdigit() and new_val.isdigit():
return I_VER_NEWER
# 4 len
if len_pre_old > len_pre_new:
return I_VER_OLDER
if len_pre_new > len_pre_old:
return I_VER_NEWER
# --------------------------------------------------------------------------
# error in one or both versions
return I_VER_SAME
dialog(message, buttons, default='', loop=False, btn_sep='/', msg_fmt='{} [{}]: ')
Create a dialog-like question and return the result
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
The message to display |
required | |
buttons
|
List of single char answers to the question |
required | |
default
|
The button item to return when the user presses Enter at the question (default: "") |
''
|
|
loop
|
If True and the user enters an invalid response, keep asking the |
False
|
|
(default
|
False) |
required | |
btn_sep
|
Char to use to separate button items |
'/'
|
|
msg_fmt
|
Format string to present message/buttons to the user |
'{} [{}]: '
|
Returns:
| Type | Description |
|---|---|
|
A lowercased string that matches a button, or an empty string under |
|
|
certain conditions |
This method returns the string entered on the command line in response to a question. If the entered option does not match any of the buttons, the question is asked again. If you set a default and the option entered is just the Return key, the default string will be returned. If no default is present, the entered string must match one of the buttons array values. All returned values are lowercased. The question will be repeatedly printed to the screen until a valid entry is made.
Note that if default == "", pressing Enter is not considered a valid entry. So if the default is empty and loop is True, the user MUST enter a valid response or the dialog will loop forever.
Source code in cnlib/cnfunctions.py
def dialog(
message, buttons, default="", loop=False, btn_sep="/", msg_fmt="{} [{}]: "
):
"""
Create a dialog-like question and return the result
Args:
message: The message to display
buttons: List of single char answers to the question
default: The button item to return when the user presses Enter at the
question (default: "")
loop: If True and the user enters an invalid response, keep asking the
question. If False, return an empty string for an invalid response
(default: False)
btn_sep: Char to use to separate button items
msg_fmt: Format string to present message/buttons to the user
Returns:
A lowercased string that matches a button, or an empty string under
certain conditions
This method returns the string entered on the command line in response to a
question. If the entered option does not match any of the buttons, the
question is asked again. If you set a default and the option entered is
just the Return key, the default string will be returned. If no default is
present, the entered string must match one of the buttons array values. All
returned values are lowercased. The question will be repeatedly printed to
the screen until a valid entry is made.
Note that if default == "", pressing Enter is not considered a valid entry.
So if the default is empty and loop is True, the user MUST enter a valid
response or the dialog will loop forever.
"""
# --------------------------------------------------------------------------
# add buttons to message
btns_all = btn_sep.join(buttons)
str_fmt = msg_fmt.format(message, btns_all)
# --------------------------------------------------------------------------
# assume loop == True
while True:
# ask the question, get the result (first char only/empty)
inp = input(str_fmt)
# if len(inp) > 0:
# inp = inp[0]
# no input (empty)
if inp == "" and default != "":
return default
# input a button
for item in buttons:
if inp.lower() == item.lower():
return item
# ----------------------------------------------------------------------
# wrong answer
# no loop, return blank
if not loop:
return ""
dpretty(dict_print, indent_size=4, indent_level=0, label=None)
Pretty print a dict
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dict_print
|
The dictionary to print |
required | |
indent_size
|
The number of spaces to use for each indent level |
4
|
|
(default
|
4) |
required | |
indent_level
|
The number of indent levels to use for this part of the |
0
|
|
print
|
process (default
|
0) |
required |
label
|
The string to use as a label (default: None) |
None
|
Returns:
| Type | Description |
|---|---|
|
The formatted string to print |
Formats a dictionary nicely so it can be printed to the console.
Source code in cnlib/cnfunctions.py
def dpretty(dict_print, indent_size=4, indent_level=0, label=None):
"""
Pretty print a dict
Args:
dict_print: The dictionary to print
indent_size: The number of spaces to use for each indent level
(default: 4)
indent_level: The number of indent levels to use for this part of the
print process (default: 0)
label: The string to use as a label (default: None)
Returns:
The formatted string to print
Raises:
OSError if the first param is not a dict
Formats a dictionary nicely so it can be printed to the console.
"""
# sanity check
if not isinstance(dict_print, dict):
raise OSError(S_ERR_NOT_DICT)
# default out
out = ""
# print label
if label is not None:
out += label + ": "
# convert indent_size to string and multiply by indent_level
indent_str = (" " * indent_size) * (indent_level)
# items will need an extra indent, since they don't recurse
indent_str_next = (" " * indent_size) * (indent_level + 1)
# default result opening brace (no indent in case it is nested and is
# preceded by a key)
out += indent_str + "{\n"
# for each entry
for k, v in dict_print.items():
# print the key
out += indent_str_next + f'"{k}": '
# if the value is a list
if isinstance(v, list):
# recurse the value and increase indent level
ret = (
lpretty(
v,
indent_size=indent_size,
indent_level=indent_level + 1,
label=None,
)
+ "\n"
)
ret = ret.lstrip()
out += ret
# if the value is a dict
elif isinstance(v, dict):
# recurse the value and increase indent level
ret = (
dpretty(
v,
indent_size=indent_size,
indent_level=indent_level + 1,
label=None,
)
+ "\n"
)
ret = ret.lstrip()
out += ret
# if it is a single entry (str, int, bool)
else:
# print the value, quoting it if it is a string
if isinstance(v, str):
out += f'"{v}",\n'
else:
out += f"{v},\n"
# get original indent
indent_str = (" " * indent_size) * indent_level
# # add closing bracket
out += indent_str + "}"
# return result
return out
get_underscore(domain, path_locale)
Return an underscore function for a module
Source code in cnlib/cnfunctions.py
def get_underscore(domain, path_locale):
"""
Return an underscore function for a module
"""
# fix locale (different than gettext stuff, mostly fixes GUI issues, but ok
# to use for CLI in the interest of common code)
locale.bindtextdomain(domain, path_locale)
# init gettext
t_translation = gettext.translation(domain, path_locale, fallback=True)
return t_translation.gettext
interpolate(val, from_min, from_max, to_min, to_max)
Convert a value in one range to the value in another range
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
val
|
The value to be converted |
required | |
from_min
|
Lower bound of input range |
required | |
from_max
|
Upper bound of input range |
required | |
to_min
|
Lower bound of output range |
required | |
to_max
|
Upper bound of output range |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
if the input value is outside the input range, or if |
Returns:
| Type | Description |
|---|---|
|
The input value as a number in the to_min/to_max range, as a float |
This method converts a value in a range to its corresponding value in another range. For example, given the values (50, 0, 100, 0, 255), it will return 127.5. Note that the return type is always a float.
Source code in cnlib/cnfunctions.py
def interpolate(val, from_min, from_max, to_min, to_max):
"""
Convert a value in one range to the value in another range
Args:
val: The value to be converted
from_min: Lower bound of input range
from_max: Upper bound of input range
to_min: Lower bound of output range
to_max: Upper bound of output range
Raises:
ValueError: if the input value is outside the input range, or if
from_min is greater than from_max, or if to_min is greater than to_max
Returns:
The input value as a number in the to_min/to_max range, as a float
This method converts a value in a range to its corresponding value in
another range. For example, given the values (50, 0, 100, 0, 255), it will
return 127.5. Note that the return type is always a float.
"""
# sanity checks
val = clamp(val, from_min, from_max)
if from_min >= from_max:
raise ValueError("from_min must be less than from_max")
if to_min >= to_max:
raise ValueError("to_min must be less than to_max")
# assume result is input
res = val
# ------------------------------------------------------------------------------
# first get the spans of the ranges (difference)
in_diff = from_max - from_min
out_diff = to_max - to_min
# get how far into the range we are
in_pos = val - from_min
# then get in val as a percent of span
in_pct = in_pos / in_diff
# get out val as a percent of span and add start val
out_pos = out_diff * in_pct
# get how far into the range we should be
res = out_pos + to_min
# ------------------------------------------------------------------------------
# return result
return res
load_paths_into_dict(paths, start_dict=None)
Combines dictionaries from all found paths
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
paths
|
The file path or list of file paths to load |
required | |
start_dict
|
The starting dict and final dict after combining (default: None) |
None
|
Returns:
| Type | Description |
|---|---|
|
The final combined dictionary |
Raises:
| Type | Description |
|---|---|
OSError
|
If the file does not exist or the file is not a valid JSON |
Load the dictionaries from all files and use combine_dicts to combine them.
Source code in cnlib/cnfunctions.py
def load_paths_into_dict(paths, start_dict=None):
"""
Combines dictionaries from all found paths
Args:
paths: The file path or list of file paths to load
start_dict: The starting dict and final dict after combining (default:\
None)
Returns:
The final combined dictionary
Raises:
OSError: If the file does not exist or the file is not a valid JSON
file
Load the dictionaries from all files and use combine_dicts to combine them.
"""
# sanity check
if not isinstance(paths, list):
paths = [paths]
# set the default result
if start_dict is None:
start_dict = {}
# loop through possible files
for path in paths:
# sanity checks
if not path:
continue
path = Path(path).resolve()
# try each option
try:
# open the file
with open(path, "r", encoding=S_ENCODING) as a_file:
# load dict from file
new_dict = json.load(a_file)
# combine new dict with previous
start_dict = combine_dicts(new_dict, start_dict)
# file not found
except FileNotFoundError as e:
raise OSError(S_ERR_NOT_FOUND.format(path)) from e
# not valid json in file
except json.JSONDecodeError as e:
raise OSError(S_ERR_NOT_JSON.format(path)) from e
# return the final dict
return start_dict
lpretty(list_print, indent_size=4, indent_level=0, label=None)
Pretty print a list
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
list_print
|
The list to print |
required | |
indent_size
|
The number of spaces to use for each indent level |
4
|
|
(default
|
4) |
required | |
indent_level
|
The number of indent levels to use for this part of the |
0
|
|
print
|
process (default
|
0) |
required |
label
|
The string to use as a label (default: None) |
None
|
Returns:
| Type | Description |
|---|---|
|
The formatted string to print |
Formats a list nicely so it can be printed to the console.
Source code in cnlib/cnfunctions.py
def lpretty(list_print, indent_size=4, indent_level=0, label=None):
"""
Pretty print a list
Args:
list_print: The list to print
indent_size: The number of spaces to use for each indent level
(default: 4)
indent_level: The number of indent levels to use for this part of the
print process (default: 0)
label: The string to use as a label (default: None)
Returns:
The formatted string to print
Raises:
OSError if the first param is not a list
Formats a list nicely so it can be printed to the console.
"""
# sanity check
if not isinstance(list_print, list):
raise OSError(S_ERR_NOT_LIST)
# default out
out = ""
# print label
if label is not None:
out += label + ": "
# convert indent_size to string and multiply by indent_level
indent_str = (" " * indent_size) * (indent_level)
# items will need an extra indent, since they don't recurse
indent_str_next = (" " * indent_size) * (indent_level + 1)
# default result opening brace (no indent in case it is nested and is
# preceded by a key)
out += indent_str + "[\n"
# for each entry
for v in list_print:
# if the value is a list
if isinstance(v, list):
# recurse the value and increase indent level
ret = (
lpretty(
v,
indent_size=indent_size,
indent_level=indent_level + 1,
label=None,
)
+ "\n"
)
out += ret
# if the value is a dict
elif isinstance(v, dict):
# recurse the value and increase indent level
ret = (
dpretty(
v,
indent_size=indent_size,
indent_level=indent_level + 1,
label=None,
)
+ "\n"
)
out += ret
# if it is a single entry (str, int, bool)
else:
# print the value, quoting it if it is a string
if isinstance(v, str):
out += indent_str_next + f'"{v}",\n'
else:
out += indent_str_next + f"{v},\n"
# get original indent
indent_str = (" " * indent_size) * indent_level
# # add closing bracket
out += indent_str + "]"
# return result
return out
pascal_case(a_str)
Format a string in Pascal case
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a_str
|
A string to convert to Pascal case |
required |
Returns; The Pascal cased string
Formats the given string to a Pascal case equivalent, ie. "my_class" becomes "MyClass".
Source code in cnlib/cnfunctions.py
def pascal_case(a_str):
"""
Format a string in Pascal case
Args:
a_str: A string to convert to Pascal case
Returns;
The Pascal cased string
Formats the given string to a Pascal case equivalent, ie. "my_class"
becomes "MyClass".
"""
# do formatting
name_pascal = a_str
name_pascal = name_pascal.replace("_", " ")
name_pascal = name_pascal.replace("-", " ")
name_pascal = name_pascal.title()
name_pascal = name_pascal.replace(" ", "")
# return result
return name_pascal
pp(obj, indent_size=4, label=None)
Pretty print a dictionary or list
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
The dictionary or list to print |
required | |
indent_size
|
The number of spaces to use for each indent level |
4
|
|
(default
|
4) |
required | |
label
|
The string to use as a label (default: None) |
None
|
Returns:
| Type | Description |
|---|---|
|
The object formatted for printing |
Formats a dictionary or list nicely and prints it to the console. Note that this method includes magic commas in the output, and therefore cannot be used to create true JSON-compatible strings. It should only be used for debugging.
Source code in cnlib/cnfunctions.py
def pp(obj, indent_size=4, label=None):
"""
Pretty print a dictionary or list
Args:
obj: The dictionary or list to print
indent_size: The number of spaces to use for each indent level
(default: 4)
label: The string to use as a label (default: None)
Returns:
The object formatted for printing
Raises:
OSError if the first param is not a dict or list
Formats a dictionary or list nicely and prints it to the console. Note that
this method includes magic commas in the output, and therefore cannot be
used to create true JSON-compatible strings. It should only be used for
debugging.
"""
# the default result
result = ""
# call different pretty functions depending on the object type
if isinstance(obj, dict):
result = dpretty(obj, indent_size, 0, label)
elif isinstance(obj, list):
result = lpretty(obj, indent_size, 0, label)
else:
raise OSError(S_ERR_NOT_DICT_OR_LIST)
# print the result
print(result)
printc(*values, sep=' ', end='\n', file=None, flush=False, fg=0, bg=0, bold=False)
Print a string in color
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*values
|
A variable number of string arguments |
()
|
|
sep
|
The string used to join *values (default: ' ') |
' '
|
|
end
|
The character(s) to print after the *values (default:'\n') |
'\n'
|
|
file
|
The file object to print to or, if None, print to stdout (default: None) |
None
|
|
flush
|
Whether to force the output buffer to write immediately, rather than waiting for it to fill |
False
|
|
fg
|
The foreground color of the text as a C_FG_XXX value (see below). If 0, use default terminal color (default: 0) |
0
|
|
bg
|
The background color of the text as a C_FG_XXX value (see below). If 0, use default terminal color (default: 0) |
0
|
|
bold
|
Whether the text is bold (duh) (default:False) |
False
|
This function prints something to the console, just like print(), but with COLOR! and BOLD!
The first five parameters are EXACTLY the same as print() and the last three are as follows:
fg: The foreground color of the text to print. This can be one of the following values:
C_FG_NONE (use the terminal default)
C_FG_BLACK
C_FG_RED
C_FG_GREEN
C_FG_YELLOW
C_FG_BLUE
C_FG_MAGENTA
C_FG_CYAN
C_FG_WHITE
bg: The background (or highlight) color of the text to print. This can be one of the following values:
C_BG_NONE (use the terminal default)
C_BG_BLACK
C_BG_RED
C_BG_GREEN
C_BG_YELLOW
C_BG_BLUE
C_BG_MAGENTA
C_BG_CYAN
C_BG_WHITE
A note about the background color:
Setting the background color will (almost?) always set the foreground color to white. So no cyan text on a magenta background.
Source code in cnlib/cnfunctions.py
def printc(
*values, sep=" ", end="\n", file=None, flush=False, fg=0, bg=0, bold=False
):
"""
Print a string in color
Args:
*values: A variable number of string arguments
sep: The string used to join *values (default: ' ')
end: The character(s) to print after the *values (default:'\\n')
file: The file object to print to or, if None, print to stdout \
(default: None)
flush: Whether to force the output buffer to write immediately, \
rather than waiting for it to fill
fg: The foreground color of the text as a C_FG_XXX value (see below). \
If 0, use default terminal color (default: 0)
bg: The background color of the text as a C_FG_XXX value (see below). \
If 0, use default terminal color (default: 0)
bold: Whether the text is bold (duh) (default:False)
This function prints something to the console, just like print(), but with
COLOR! and BOLD!\n
The first five parameters are EXACTLY the same as print()
and the last three are as follows:\n
\n
fg: The foreground color of the text to print. This can be one of the
following values:\n
\n
C_FG_NONE (use the terminal default)\n
C_FG_BLACK\n
C_FG_RED\n
C_FG_GREEN\n
C_FG_YELLOW\n
C_FG_BLUE\n
C_FG_MAGENTA\n
C_FG_CYAN\n
C_FG_WHITE\n
\n
bg: The background (or highlight) color of the text to print. This can
be one of the following values:\n
\n
C_BG_NONE (use the terminal default)\n
C_BG_BLACK\n
C_BG_RED\n
C_BG_GREEN\n
C_BG_YELLOW\n
C_BG_BLUE\n
C_BG_MAGENTA\n
C_BG_CYAN\n
C_BG_WHITE\n
\n
A note about the background color:\n
Setting the background color will (almost?) always set the foreground color
to white. So no cyan text on a magenta background.
"""
# NB: every option has a unique value and order does not matter
# the default array of text options
arr_opt = []
# maybe set fg
if fg != 0:
arr_opt.append(str(fg))
# maybe set bg
if bg != 0:
arr_opt.append(str(bg))
# maybe set bold
if bold:
arr_opt.append("1")
# put arr_opt together
color_val = ";".join(arr_opt) # '32;42;1', '42'
# get open sequence
color_start = f"\033[{color_val}m"
# get the close sequence (reset fg/bg/bold)
color_end = "\033[0m"
# --------------------------------------------------------------------------
# get the full string
value = sep.join(values)
# split into lines
lines = value.split("\n")
# a list of newline-separated strings in the final string
wrapped_lines = []
# for each string
for line in lines:
# wrap it and add it to the final string
wrapped_lines.append(color_start + line + color_end)
# rejoin strings using newline
final_str = "\n".join(wrapped_lines)
print(final_str, sep=sep, end=end, file=file, flush=flush)
printd(*values, sep=' ', end='\n', file=None, flush=False)
Print a string if the debug param is True
Args:
*values: A variable number of string arguments
sep: The string used to join *values (default: ' ')
end: The character(s) to print after the *values (default:'
') file: The file object to print to or, if None, print to stdout (default: None) flush: Whether to force the output buffer to write immediately, rather than waiting for it to fill
This function is really handy for me when I run a program in debug mode. It
just lets me wrap prints in context-aware statements.
Source code in cnlib/cnfunctions.py
def printd(*values, sep=" ", end="\n", file=None, flush=False):
"""
Print a string if the debug param is True
Args:
*values: A variable number of string arguments
sep: The string used to join *values (default: ' ')
end: The character(s) to print after the *values (default:'\n')
file: The file object to print to or, if None, print to stdout
(default: None)
flush: Whether to force the output buffer to write immediately, rather
than waiting for it to fill
This function is really handy for me when I run a program in debug mode. It
just lets me wrap prints in context-aware statements.
"""
if B_DEBUG:
printc(
*values,
sep=sep,
end=end,
file=file,
flush=flush,
fg=C_FG_RED,
bold=True,
)
run(cmd, shell=False, capture_output=False)
Run a program or shell command string
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cmd
|
The command to run |
required | |
shell
|
If False (the default), run the cmd as one long string. If True, |
False
|
Returns:
| Type | Description |
|---|---|
|
The result of running the command line, as a |
|
|
subprocess.CompletedProcess object |
This is just a dumb convenience method to use subprocess with a string instead of having to convert a string to an array with shlex every time. It also combines FileNotFoundError and CalledProcessError into one CNRunError.
Source code in cnlib/cnfunctions.py
def run(cmd, shell=False, capture_output=False):
"""
Run a program or shell command string
Args:
cmd: The command to run
shell: If False (the default), run the cmd as one long string. If True,
split the cmd into separate arguments
Returns:
The result of running the command line, as a
subprocess.CompletedProcess object
Raises:
CNRunError if the command is invalid or the process fails
This is just a dumb convenience method to use subprocess with a string
instead of having to convert a string to an array with shlex every time. It
also combines FileNotFoundError and CalledProcessError into one CNRunError.
"""
# sanity check to make sure cmd is a string
cmd = str(cmd)
# if not shell, split into bin/cli options
# NB: if shell, we only want the stuff after ["sh", "-c"], which is what
# would be typed at the command prompt (i.e. all one long command string)
if not shell:
cmd = shlex.split(cmd)
# get result of running the shell command or bubble up an error
try:
cp = subprocess.run(
# the cmd or array of commands
cmd,
# whether the call is a file w/ params (False) or a direct shell
# input (True)
shell=shell,
# put stdout/stderr into cp/cpe, instead of the current terminal
capture_output=capture_output,
# if check is True, an exception will be raised if the return code
# is not 0
# if check is False, no exception is raised but cp will be None,
# meaning you have to test for it in the calling function
# but that also means you have no information on WHY it failed
# because stderr comes from the CalledProcessError IF
# capture_output=True
check=True,
# convert stdout/stderr from bytes to text
encoding=S_ENCODING,
text=True,
)
# return the result
return cp
# the first item in the list is not a bin
# NB: try these:
# cmd = "cd /", shell = False: fail
# cmd = "cd /", shell = True: pass
# cmd = "ls -l", shell = False: pass
# cmd = "ls -l", shell = True: pass
# check if first item is bin, if shell false
except FileNotFoundError as fnfe:
# NB: fnfe already has a nice __str__, so use that in the stderr
# also output = stdout, which is kinda pointless for this error
# NB: print(exc) gives ALL properties
# print(exc.stderr) give concise output
exc = CNRunError(
cmd, fnfe.errno, fnfe.filename, f"{fnfe}", fnfe.filename
)
raise exc from fnfe
# cmd ran but failed
except subprocess.CalledProcessError as cpe:
# NB: here we use the regular stderr as concise output
exc = CNRunError(
cpe.cmd, cpe.returncode, cpe.stdout, cpe.stderr, cpe.output
)
raise exc from cpe
save_dict_into_paths(save_dict, paths)
Save a dictionary to all paths
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
save_dict
|
The dictionary to save to the paths(s) |
required | |
paths
|
The path or list of paths to save to |
required |
Raises:
| Type | Description |
|---|---|
OSError
|
If the file does not exist and can't be created |
Save the dictionary to a file at all the specified locations.
Source code in cnlib/cnfunctions.py
def save_dict_into_paths(save_dict, paths):
"""
Save a dictionary to all paths
Args:
save_dict: The dictionary to save to the paths(s)
paths: The path or list of paths to save to
Raises:
OSError: If the file does not exist and can't be created
Save the dictionary to a file at all the specified locations.
"""
# sanity checks
if not isinstance(paths, list):
paths = [paths]
if len(paths) == 0:
return
# loop through possible files
for path in paths:
# sanity checks
if not path:
continue
path = Path(path).resolve()
# try each path
try:
# first make dirs
path.parent.mkdir(parents=True, exist_ok=True)
# open the file
with open(path, "w", encoding=S_ENCODING) as a_file:
# save dict tp file
json.dump(save_dict, a_file, indent=4)
# raise an OS Error
except OSError as e:
raise OSError(S_ERR_NOT_CREATE.format(path)) from e