Make the common module a little simpler

This commit is contained in:
Veronica Berglyd Olsen
2022-10-19 22:10:23 +02:00
parent efb7d12e1d
commit cfe11f7b54
3 changed files with 106 additions and 139 deletions
+99 -127
View File
@@ -216,25 +216,25 @@ def formatInt(value):
return str(value) return str(value)
def formatTimeStamp(theTime, fileSafe=False): def formatTimeStamp(value, fileSafe=False):
"""Take a number (on the format returned by time.time()) and convert """Take a number (on the format returned by time.time()) and convert
it to a timestamp string. it to a timestamp string.
""" """
if fileSafe: if fileSafe:
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_FSTAMP) return datetime.fromtimestamp(value).strftime(nwConst.FMT_FSTAMP)
else: else:
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_TSTAMP) return datetime.fromtimestamp(value).strftime(nwConst.FMT_TSTAMP)
def formatTime(tS): def formatTime(t):
"""Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format """Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
if a full day or longer. if a full day or longer.
""" """
if isinstance(tS, int): if isinstance(t, int):
if tS >= 86400: if t >= 86400:
return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" return f"{t//86400:d}-{t%86400//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
else: else:
return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" return f"{t//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
return "ERROR" return "ERROR"
@@ -276,111 +276,111 @@ def splitVersionNumber(value):
return vMajor, vMinor, vPatch, vInt return vMajor, vMinor, vPatch, vInt
def transferCase(theSource, theTarget): def transferCase(source, target):
"""Transfers the case of the source word to the target word. This """Transfers the case of the source word to the target word. This
will consider all upper or lower, and first char capitalisation. will consider all upper or lower, and first char capitalisation.
""" """
theResult = theTarget theResult = target
if not isinstance(theSource, str) or not isinstance(theTarget, str): if not isinstance(source, str) or not isinstance(target, str):
return theResult return theResult
if len(theTarget) < 1 or len(theSource) < 1: if len(target) < 1 or len(source) < 1:
return theResult return theResult
if theSource.istitle(): if source.istitle():
theResult = theTarget.title() theResult = target.title()
if theSource.isupper(): if source.isupper():
theResult = theTarget.upper() theResult = target.upper()
elif theSource.islower(): elif source.islower():
theResult = theTarget.lower() theResult = target.lower()
return theResult return theResult
def fuzzyTime(secDiff): def fuzzyTime(seconds):
"""Converts a time difference in seconds into a fuzzy time string. """Converts a time difference in seconds into a fuzzy time string.
""" """
if secDiff < 0: if seconds < 0:
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "in the future" "Common", "in the future"
) )
elif secDiff < 30: elif seconds < 30:
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "just now" "Common", "just now"
) )
elif secDiff < 90: elif seconds < 90:
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a minute ago" "Common", "a minute ago"
) )
elif secDiff < 3300: # 55 minutes elif seconds < 3300: # 55 minutes
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} minutes ago" "Common", "{0} minutes ago"
).format(int(round(secDiff/60))) ).format(int(round(seconds/60)))
elif secDiff < 5400: # 90 minutes elif seconds < 5400: # 90 minutes
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "an hour ago" "Common", "an hour ago"
) )
elif secDiff < 84600: # 23.5 hours elif seconds < 84600: # 23.5 hours
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} hours ago" "Common", "{0} hours ago"
).format(int(round(secDiff/3600))) ).format(int(round(seconds/3600)))
elif secDiff < 129600: # 1.5 days elif seconds < 129600: # 1.5 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a day ago" "Common", "a day ago"
) )
elif secDiff < 561600: # 6.5 days elif seconds < 561600: # 6.5 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} days ago" "Common", "{0} days ago"
).format(int(round(secDiff/86400))) ).format(int(round(seconds/86400)))
elif secDiff < 907200: # 10.5 days elif seconds < 907200: # 10.5 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a week ago" "Common", "a week ago"
) )
elif secDiff < 2419200: # 28 days elif seconds < 2419200: # 28 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} weeks ago" "Common", "{0} weeks ago"
).format(int(round(secDiff/604800))) ).format(int(round(seconds/604800)))
elif secDiff < 3888000: # 45 days elif seconds < 3888000: # 45 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a month ago" "Common", "a month ago"
) )
elif secDiff < 29808000: # 345 days elif seconds < 29808000: # 345 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} months ago" "Common", "{0} months ago"
).format(int(round(secDiff/2592000))) ).format(int(round(seconds/2592000)))
elif secDiff < 47336400: # 1.5 years elif seconds < 47336400: # 1.5 years
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a year ago" "Common", "a year ago"
) )
else: else:
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} years ago" "Common", "{0} years ago"
).format(int(round(secDiff/31557600))) ).format(int(round(seconds/31557600)))
def numberToRoman(numVal, toLower=False): def numberToRoman(value, toLower=False):
"""Convert an integer to a Roman number. """Convert an integer to a Roman number.
""" """
if not isinstance(numVal, int): if not isinstance(value, int):
return "NAN" return "NAN"
if numVal < 1 or numVal > 4999: if value < 1 or value > 4999:
return "OOR" return "OOR"
theValues = [ lookup = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"), (1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"), (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
] ]
romNum = "" roman = ""
for theDiv, theSym in theValues: for divisor, symbol in lookup:
n = numVal//theDiv n = value//divisor
romNum += n*theSym roman += n*symbol
numVal -= n*theDiv value -= n*divisor
if numVal <= 0: if value <= 0:
break break
return romNum.lower() if toLower else romNum return roman.lower() if toLower else roman
# =============================================================================================== # # =============================================================================================== #
@@ -438,70 +438,70 @@ def jsonEncode(data, n=0, nmax=0):
# File and File System Functions # File and File System Functions
# =============================================================================================== # # =============================================================================================== #
def readTextFile(filePath): def readTextFile(path):
"""Read the content of a text file in a robust manner. """Read the content of a text file in a robust manner.
""" """
if not os.path.isfile(filePath): if not os.path.isfile(path):
return "" return ""
fileText = "" text = ""
try: try:
with open(filePath, mode="r", encoding="utf-8") as inFile: with open(path, mode="r", encoding="utf-8") as inFile:
fileText = inFile.read() text = inFile.read()
except Exception: except Exception:
logger.error("Could not read file: %s", filePath) logger.error("Could not read file: %s", path)
logException() logException()
return "" return ""
return fileText return text
def makeFileNameSafe(value): def makeFileNameSafe(value):
"""Returns a filename safe string of the value. """Returns a filename safe string of the value.
""" """
cleanName = "" clean = ""
for c in str(value).strip(): for c in str(value).strip():
if c.isalpha() or c.isdigit() or c == " ": if c.isalpha() or c.isdigit() or c == " ":
cleanName += c clean += c
return cleanName return clean
def ensureFolder(dirPath, parentPath=None, errLog=None): def ensureFolder(path, parent=None, errLog=None):
"""Make sure a folder exists, and if it doesn't, create it. """Make sure a folder exists, and if it doesn't, create it.
""" """
try: try:
if parentPath: if parent:
dirPath = os.path.join(parentPath, dirPath) path = os.path.join(parent, path)
if not os.path.isdir(dirPath): if not os.path.isdir(path):
os.mkdir(dirPath) os.mkdir(path)
except Exception as exc: except Exception as exc:
logger.error("Could not create folder: %s", dirPath) logger.error("Could not create folder: %s", path)
logException() logException()
if isinstance(errLog, list): if isinstance(errLog, list):
errLog.append(f"Could not create folder: {dirPath}") errLog.append(f"Could not create folder: {path}")
errLog.append(formatException(exc)) errLog.append(formatException(exc))
return False return False
return True return True
def sha256sum(filePath): def sha256sum(path):
"""Make a shasum of a file using a buffer. """Make a shasum of a file using a buffer.
Based on: https://stackoverflow.com/a/44873382/5825851 Based on: https://stackoverflow.com/a/44873382/5825851
""" """
hDigest = hashlib.sha256() digest = hashlib.sha256()
bData = bytearray(65536) bData = bytearray(65536)
mData = memoryview(bData) mData = memoryview(bData)
try: try:
with open(filePath, mode="rb", buffering=0) as inFile: with open(path, mode="rb", buffering=0) as inFile:
for n in iter(lambda: inFile.readinto(mData), 0): for n in iter(lambda: inFile.readinto(mData), 0):
hDigest.update(mData[:n]) digest.update(mData[:n])
except Exception: except Exception:
logger.error("Could not create sha256sum of: %s", filePath) logger.error("Could not create sha256sum of: %s", path)
logException() logException()
return None return None
return hDigest.hexdigest() return digest.hexdigest()
# =============================================================================================== # # =============================================================================================== #
@@ -523,87 +523,59 @@ def getGuiItem(objName):
class NWConfigParser(ConfigParser): class NWConfigParser(ConfigParser):
CNF_STR = 0
CNF_INT = 1
CNF_FLOAT = 2
CNF_BOOL = 3
CNF_S_LST = 4
CNF_I_LST = 5
def __init__(self): def __init__(self):
super().__init__() super().__init__()
def rdStr(self, section, option, default): def rdStr(self, section, option, default):
"""Read string value. """Read string value.
""" """
return self._parseLine(section, option, default, self.CNF_STR) return self.get(section, option, fallback=default)
def rdInt(self, section, option, default): def rdInt(self, section, option, default):
"""Read integer value. """Read integer value.
""" """
return self._parseLine(section, option, default, self.CNF_INT) try:
return self.getint(section, option, fallback=default)
except ValueError:
logger.error("Could not read '%s':'%s' from config", section, option)
return default
def rdFlt(self, section, option, default): def rdFlt(self, section, option, default):
"""Read float value. """Read float value.
""" """
return self._parseLine(section, option, default, self.CNF_FLOAT) try:
return self.getfloat(section, option, fallback=default)
except ValueError:
logger.error("Could not read '%s':'%s' from config", section, option)
return default
def rdBool(self, section, option, default): def rdBool(self, section, option, default):
"""Read boolean value. """Read boolean value.
""" """
return self._parseLine(section, option, default, self.CNF_BOOL) try:
return self.getboolean(section, option, fallback=default)
except ValueError:
logger.error("Could not read '%s':'%s' from config", section, option)
return default
def rdStrList(self, section, option, default): def rdStrList(self, section, option, default):
"""Read string list. """Read string list.
""" """
return self._parseLine(section, option, default, self.CNF_S_LST) result = default.copy() if isinstance(default, list) else []
if self.has_option(section, option):
data = self.get(section, option, fallback="").split(",")
for i in range(min(len(data), len(result))):
result[i] = data[i].strip()
return result
def rdIntList(self, section, option, default): def rdIntList(self, section, option, default):
"""Read integer list. """Read integer list.
""" """
return self._parseLine(section, option, default, self.CNF_I_LST) result = default.copy() if isinstance(default, list) else []
##
# Internal Functions
##
def _unpackList(self, value, default, type):
"""Unpack a comma-separated string of items into a list.
"""
inList = value.split(",")
outList = []
if isinstance(default, list):
outList = default.copy()
for i in range(min(len(inList), len(outList))):
try:
if type == self.CNF_S_LST:
outList[i] = inList[i].strip()
elif type == self.CNF_I_LST:
outList[i] = int(inList[i].strip())
except Exception:
continue
return outList
def _parseLine(self, section, option, default, type):
"""Parse a line and return the correct datatype.
"""
if self.has_option(section, option): if self.has_option(section, option):
try: data = self.get(section, option, fallback="").split(",")
if type == self.CNF_STR: for i in range(min(len(data), len(result))):
return self.get(section, option) result[i] = checkInt(data[i].strip(), result[i])
elif type == self.CNF_INT: return result
return self.getint(section, option)
elif type == self.CNF_FLOAT:
return self.getfloat(section, option)
elif type == self.CNF_BOOL:
return self.getboolean(section, option)
elif type in (self.CNF_I_LST, self.CNF_S_LST):
return self._unpackList(self.get(section, option), default, type)
except ValueError:
logger.error("Could not read '%s':'%s' from config", str(section), str(option))
logException()
return default
return default
# END Class NWConfigParser # END Class NWConfigParser
+2 -2
View File
@@ -306,8 +306,8 @@ class Config:
# We don't error on these failing since they are not essential # We don't error on these failing since they are not essential
if self.dataPath is not None: if self.dataPath is not None:
ensureFolder("syntax", parentPath=self.dataPath) ensureFolder("syntax", parent=self.dataPath)
ensureFolder("themes", parentPath=self.dataPath) ensureFolder("themes", parent=self.dataPath)
# Check if config file exists # Check if config file exists
if self.confPath is not None: if self.confPath is not None:
+5 -10
View File
@@ -556,13 +556,13 @@ def testBaseCommon_EnsureFolder(monkeypatch, fncDir):
assert ensureFolder(newDir1) is True assert ensureFolder(newDir1) is True
assert os.path.isdir(newDir1) assert os.path.isdir(newDir1)
assert ensureFolder("newDir2", parentPath=fncDir) is True assert ensureFolder("newDir2", parent=fncDir) is True
assert os.path.isdir(newDir2) assert os.path.isdir(newDir2)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("os.mkdir", causeOSError) mp.setattr("os.mkdir", causeOSError)
errLog = [] errLog = []
assert ensureFolder("newDir3", parentPath=fncDir, errLog=errLog) is False assert ensureFolder("newDir3", parent=fncDir, errLog=errLog) is False
assert errLog[0] == f"Could not create folder: {newDir3}" assert errLog[0] == f"Could not create folder: {newDir3}"
assert not os.path.isdir(newDir3) assert not os.path.isdir(newDir3)
@@ -673,10 +673,10 @@ def testBaseCommon_NWConfigParser(fncDir):
# Read Float # Read Float
assert cfgParser.rdFlt("main", "intopt1", 13.0) == 42.0 assert cfgParser.rdFlt("main", "intopt1", 13.0) == 42.0
assert cfgParser.rdFlt("main", "float1", 13.0) == 4.2 assert cfgParser.rdFlt("main", "float1", 13.0) == 4.2
assert cfgParser.rdInt("main", "stropt", 13.0) == 13.0 assert cfgParser.rdFlt("main", "stropt", 13.0) == 13.0
assert cfgParser.rdInt("nope", "intopt1", 13.0) == 13.0 assert cfgParser.rdFlt("nope", "intopt1", 13.0) == 13.0
assert cfgParser.rdInt("main", "blabla", 13.0) == 13.0 assert cfgParser.rdFlt("main", "blabla", 13.0) == 13.0
# Read String List # Read String List
assert cfgParser.rdStrList("main", "list1", []) == [] assert cfgParser.rdStrList("main", "list1", []) == []
@@ -704,9 +704,4 @@ def testBaseCommon_NWConfigParser(fncDir):
assert cfgParser.rdIntList("nope", "list2", [1]) == [1] assert cfgParser.rdIntList("nope", "list2", [1]) == [1]
assert cfgParser.rdIntList("main", "blabla", [1]) == [1] assert cfgParser.rdIntList("main", "blabla", [1]) == [1]
# Internal
# ========
assert cfgParser._parseLine("main", "stropt", None, 999) is None
# END Test testBaseCommon_NWConfigParser # END Test testBaseCommon_NWConfigParser