util.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. # -*- coding: utf-8 -*-
  2. #
  3. # AWL simulator - common utility functions
  4. #
  5. # Copyright 2012-2017 Michael Buesch <m@bues.ch>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. #
  21. from __future__ import division, absolute_import, print_function, unicode_literals
  22. from awlsim.common.compat import *
  23. from awlsim.common.enumeration import *
  24. from awlsim.common.exceptions import *
  25. import sys
  26. import os
  27. import errno
  28. import random
  29. import base64
  30. import binascii
  31. import functools
  32. import itertools
  33. from collections import deque
  34. __all__ = [
  35. "functools",
  36. "itertools",
  37. "deque",
  38. "Logging",
  39. "printDebug",
  40. "printVerbose",
  41. "printInfo",
  42. "printWarning",
  43. "printError",
  44. "fileExists",
  45. "safeFileRead",
  46. "safeFileWrite",
  47. "strPartitionFull",
  48. "str2bool",
  49. "strToBase64",
  50. "base64ToStr",
  51. "bytesToHexStr",
  52. "toUnixEol",
  53. "toDosEol",
  54. "isInteger",
  55. "isString",
  56. "strEqual",
  57. "isiterable",
  58. "getfirst",
  59. "getany",
  60. "toList",
  61. "toSet",
  62. "pivotDict",
  63. "listIndex",
  64. "listToHumanStr",
  65. "listExpand",
  66. "clamp",
  67. "math_gcd",
  68. "math_lcm",
  69. "nopContext",
  70. "RelPath",
  71. "shortUUID",
  72. ]
  73. class Logging(object):
  74. EnumGen.start
  75. LOG_NONE = EnumGen.item
  76. LOG_ERROR = EnumGen.item
  77. LOG_WARNING = EnumGen.item
  78. LOG_INFO = EnumGen.item
  79. LOG_VERBOSE = EnumGen.item
  80. LOG_DEBUG = EnumGen.item
  81. EnumGen.end
  82. loglevel = LOG_INFO
  83. prefix = ""
  84. @classmethod
  85. def setLoglevel(cls, loglevel):
  86. if loglevel not in (cls.LOG_NONE,
  87. cls.LOG_ERROR,
  88. cls.LOG_WARNING,
  89. cls.LOG_INFO,
  90. cls.LOG_VERBOSE,
  91. cls.LOG_DEBUG):
  92. raise AwlSimError("Invalid log level '%d'" % loglevel)
  93. cls.loglevel = loglevel
  94. @classmethod
  95. def setPrefix(cls, prefix):
  96. cls.prefix = prefix
  97. @classmethod
  98. def __print(cls, stream, text):
  99. with contextlib.suppress(RuntimeError):
  100. if stream:
  101. if cls.prefix:
  102. stream.write(cls.prefix)
  103. stream.write(text)
  104. stream.write("\n")
  105. stream.flush()
  106. @classmethod
  107. def printDebug(cls, text):
  108. if cls.loglevel >= cls.LOG_DEBUG:
  109. cls.__print(sys.stdout, text)
  110. @classmethod
  111. def printVerbose(cls, text):
  112. if cls.loglevel >= cls.LOG_VERBOSE:
  113. cls.__print(sys.stdout, text)
  114. @classmethod
  115. def printInfo(cls, text):
  116. if cls.loglevel >= cls.LOG_INFO:
  117. cls.__print(sys.stdout, text)
  118. @classmethod
  119. def printWarning(cls, text):
  120. if cls.loglevel >= cls.LOG_WARNING:
  121. cls.__print(sys.stderr, text)
  122. @classmethod
  123. def printError(cls, text):
  124. if cls.loglevel >= cls.LOG_ERROR:
  125. cls.__print(sys.stderr, text)
  126. def printDebug(text):
  127. Logging.printDebug(text)
  128. def printVerbose(text):
  129. Logging.printVerbose(text)
  130. def printInfo(text):
  131. Logging.printInfo(text)
  132. def printWarning(text):
  133. Logging.printWarning(text)
  134. def printError(text):
  135. Logging.printError(text)
  136. def fileExists(filename):
  137. """Returns True, if the file exists.
  138. Returns False, if the file does not exist.
  139. Returns None, if another error occurred.
  140. """
  141. try:
  142. os.stat(filename)
  143. except OSError as e:
  144. if e.errno == errno.ENOENT:
  145. return False
  146. return None
  147. return True
  148. def safeFileRead(filename):
  149. try:
  150. with open(filename, "rb") as fd:
  151. data = fd.read()
  152. fd.close()
  153. except IOError as e:
  154. raise AwlSimError("Failed to read '%s': %s" %\
  155. (filename, str(e)))
  156. return data
  157. def safeFileWrite(filename, data):
  158. for count in range(1000):
  159. tmpFile = "%s-%d-%d.tmp" %\
  160. (filename, random.randint(0, 0xFFFF), count)
  161. if not os.path.exists(tmpFile):
  162. break
  163. else:
  164. raise AwlSimError("Could not create temporary file")
  165. try:
  166. with open(tmpFile, "wb") as fd:
  167. fd.write(data)
  168. fd.flush()
  169. fd.close()
  170. if not osIsPosix:
  171. # Can't use safe rename on non-POSIX.
  172. # Must unlink first.
  173. with contextlib.suppress(OSError):
  174. os.unlink(filename)
  175. os.rename(tmpFile, filename)
  176. except (IOError, OSError) as e:
  177. raise AwlSimError("Failed to write file:\n" + str(e))
  178. finally:
  179. with contextlib.suppress(IOError, OSError):
  180. os.unlink(tmpFile)
  181. # Fully partition a string by separator 'sep'.
  182. # Returns a list of strings:
  183. # [ "first-element", sep, "second-element", sep, ... ]
  184. # If 'keepEmpty' is True, empty elements are kept.
  185. def strPartitionFull(string, sep, keepEmpty=True):
  186. first, ret = True, []
  187. for elem in string.split(sep):
  188. if not first:
  189. ret.append(sep)
  190. if elem or keepEmpty:
  191. ret.append(elem)
  192. first = False
  193. return ret
  194. def str2bool(string, default=False):
  195. """Convert a human readable string to a boolean.
  196. """
  197. s = string.lower().strip()
  198. if s in {"true", "yes", "on", "enable", "enabled"}:
  199. return True
  200. if s in {"false", "no", "off", "disable", "disabled"}:
  201. return False
  202. try:
  203. return bool(int(s, 10))
  204. except ValueError:
  205. return default
  206. def strToBase64(string, ignoreErrors=False):
  207. """Convert a string to a base64 encoded ascii string.
  208. Throws ValueError on errors, if ignoreErrors is False."""
  209. try:
  210. b = string.encode("utf-8", "ignore" if ignoreErrors else "strict")
  211. return base64.b64encode(b).decode("ascii")
  212. except (UnicodeError, binascii.Error, TypeError) as e:
  213. if ignoreErrors:
  214. return ""
  215. raise ValueError
  216. def base64ToStr(b64String, ignoreErrors=False):
  217. """Convert a base64 encoded ascii string to utf-8 string.
  218. Throws ValueError on errors, if ignoreErrors is False."""
  219. try:
  220. b = b64String.encode("ascii",
  221. "ignore" if ignoreErrors else "strict")
  222. return base64.b64decode(b).decode("utf-8",
  223. "ignore" if ignoreErrors else "strict")
  224. except (UnicodeError, binascii.Error, TypeError) as e:
  225. if ignoreErrors:
  226. return ""
  227. raise ValueError
  228. def bytesToHexStr(_bytes):
  229. """Convert bytes to a hex-string.
  230. """
  231. if _bytes is None:
  232. return None
  233. return binascii.b2a_hex(_bytes).decode("ascii")
  234. def toUnixEol(string):
  235. """Convert a string to UNIX line endings,
  236. no matter what line endings (mix) the input string is.
  237. """
  238. return string.replace("\r\n", "\n")\
  239. .replace("\r", "\n")
  240. def toDosEol(string):
  241. """Convert a string to DOS line endings,
  242. no matter what line endings (mix) the input string is.
  243. """
  244. return toUnixEol(string).replace("\n", "\r\n")
  245. def __isInteger_python2(value):
  246. return isinstance(value, int) or\
  247. isinstance(value, long)
  248. def __isInteger_python3(value):
  249. return isinstance(value, int)
  250. isInteger = py23(__isInteger_python2,
  251. __isInteger_python3)
  252. def __isString_python2(value):
  253. return isinstance(value, unicode) or\
  254. isinstance(value, str)
  255. def __isString_python3(value):
  256. return isinstance(value, str)
  257. isString = py23(__isString_python2,
  258. __isString_python3)
  259. def strEqual(string0, string1, caseSensitive=True):
  260. """Compare string0 to string1.
  261. If caseSensitive is False, case is ignored.
  262. Returns True, if both strings are equal.
  263. """
  264. if not caseSensitive:
  265. if hasattr(string0, "casefold"):
  266. string0, string1 = string0.casefold(), string1.casefold()
  267. else:
  268. string0, string1 = string0.lower(), string1.lower()
  269. return string0 == string1
  270. def isiterable(obj):
  271. """Check if an object is iterable.
  272. """
  273. try:
  274. iter(obj)
  275. return True
  276. except TypeError:
  277. pass
  278. return False
  279. def getfirst(iterable, exception=KeyError):
  280. """Get the first item from an iterable.
  281. This also works for generators.
  282. If the iterable is empty, exception is raised.
  283. If exception is None, None is returned instead.
  284. Warning: If iterable is not indexable (for example a set),
  285. an arbitrary item is returned instead.
  286. """
  287. try:
  288. return next(iter(iterable))
  289. except StopIteration:
  290. if exception:
  291. raise exception
  292. return None
  293. # Get an arbitrary item from an iterable.
  294. # If the iterable is empty, exception is raised.
  295. # If exception is None, None is returned instead.
  296. getany = getfirst
  297. def toList(value):
  298. """Returns value, if value is a list.
  299. Returns a list with the elements of value, if value is a set.
  300. Returns a list with the elements of value, if value is a frozenset.
  301. Returns a list with the elements of value, if value is an iterable, but not a string.
  302. Otherwise returns a list with value as element.
  303. """
  304. if isinstance(value, list):
  305. return value
  306. if isinstance(value, set):
  307. return sorted(value)
  308. if isinstance(value, frozenset):
  309. return sorted(value)
  310. if not isString(value):
  311. if isiterable(value):
  312. return list(value)
  313. return [ value, ]
  314. # Returns value, if value is a set.
  315. # Returns a set, if value is a frozenset.
  316. # Returns a set with the elements of value, if value is a tuple.
  317. # Returns a set with the elements of value, if value is a list.
  318. # Otherwise returns a set with value as single element.
  319. def toSet(value):
  320. if isinstance(value, set):
  321. return value
  322. if isinstance(value, frozenset):
  323. return set(value)
  324. if isinstance(value, list) or\
  325. isinstance(value, tuple):
  326. return set(value)
  327. return { value, }
  328. def pivotDict(inDict):
  329. outDict = {}
  330. for key, value in dictItems(inDict):
  331. if value in outDict:
  332. raise KeyError("Ambiguous key in pivot dict")
  333. outDict[value] = key
  334. return outDict
  335. # Returns the index of a list element, or -1 if not found.
  336. # If translate if not None, it should be a callable that translates
  337. # a list entry. Arguments are index, entry.
  338. def listIndex(_list, value, start=0, stop=-1, translate=None):
  339. if stop < 0:
  340. stop = len(_list)
  341. if translate:
  342. for i, ent in enumerate(_list[start:stop], start):
  343. if translate(i, ent) == value:
  344. return i
  345. return -1
  346. try:
  347. return _list.index(value, start, stop)
  348. except ValueError:
  349. return -1
  350. # Convert an integer list to a human readable string.
  351. # Example: [1, 2, 3] -> "1, 2 or 3"
  352. def listToHumanStr(lst, lastSep="or"):
  353. if not lst:
  354. return ""
  355. lst = toList(lst)
  356. string = ", ".join(str(i) for i in lst)
  357. # Replace last comma with 'lastSep'
  358. string = string[::-1].replace(",", lastSep[::-1] + " ", 1)[::-1]
  359. return string
  360. # Expand the elements of a list.
  361. # 'expander' is the expansion callback. 'expander' takes
  362. # one list element as argument. It returns a list.
  363. def listExpand(lst, expander):
  364. ret = []
  365. for item in lst:
  366. ret.extend(expander(item))
  367. return ret
  368. def clamp(value, minValue, maxValue):
  369. """Clamp value to the range minValue-maxValue.
  370. ValueError is raised, if minValue is bigger than maxValue.
  371. """
  372. if minValue > maxValue:
  373. raise ValueError
  374. return max(min(value, maxValue), minValue)
  375. # Get "Greatest Common Divisor"
  376. def math_gcd(*args):
  377. return reduce(compat_gcd, args)
  378. # Get "Least Common Multiple"
  379. def math_lcm(*args):
  380. return reduce(lambda x, y: x * y // math_gcd(x, y),
  381. args)
  382. class nopContextManager(object):
  383. """No-operation context manager.
  384. """
  385. def __enter__(self):
  386. return None
  387. def __exit__(self, exctype, excinst, exctb):
  388. return False
  389. nopContext = nopContextManager()
  390. class RelPath(object):
  391. def __init__(self, relativeToDir):
  392. self.__relativeToDir = relativeToDir
  393. def toRelative(self, path):
  394. """Generate an OS-independent relative string from a path."""
  395. path = os.path.relpath(path, self.__relativeToDir)
  396. if os.path.splitdrive(path)[0]:
  397. raise AwlSimError("Failed to strip the drive letter from a path, "
  398. "because the base and the path don't reside on the "
  399. "same drive. Please make sure the base and the path "
  400. "reside on the same drive.\n"
  401. "Base: %s\n"
  402. "Path: %s" % (
  403. self.__relativeToDir, path))
  404. path = path.replace(os.path.sep, "/")
  405. return path
  406. def fromRelative(self, path):
  407. """Generate a path from an OS-independent relative string."""
  408. path = path.replace("/", os.path.sep)
  409. path = os.path.join(self.__relativeToDir, path)
  410. return path
  411. def shortUUID(uuidStr):
  412. """Shorten an uuid string.
  413. """
  414. uuidStr = str(uuidStr).strip()
  415. if len(uuidStr) == 36 and\
  416. uuidStr[8] == '-' and\
  417. uuidStr[13] == '-' and\
  418. uuidStr[18] == '-' and\
  419. uuidStr[23] == '-':
  420. uuidStr = uuidStr[0:8] + ".." + uuidStr[-6:-1]
  421. return uuidStr