File indexing completed on 2024-04-21 04:01:50

0001 # -*- coding: utf-8 -*-
0002 
0003 """
0004 Copyright (C) 2008-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
0005 
0006 SPDX-License-Identifier: GPL-2.0
0007 
0008 """
0009 
0010 import logging
0011 import os
0012 import string
0013 
0014 from locale import getpreferredencoding
0015 from sys import _getframe
0016 
0017 # util must not import twisted or we need to change kajongg.py
0018 
0019 from common import Internal, Debug # pylint: disable=redefined-builtin
0020 from qt import Qt, QEvent
0021 from util import elapsedSince, traceback, gitHead, callers
0022 from mi18n import i18n
0023 from dialogs import Sorry, Information, NoPrompt
0024 
0025 
0026 SERVERMARK = '&&SERVER&&'
0027 
0028 
0029 class Fmt(string.Formatter):
0030 
0031     """this formatter can parse {id(x)} and output a short ascii form for id"""
0032     alphabet = string.ascii_uppercase + string.ascii_lowercase
0033     base = len(alphabet)
0034     formatter = None
0035 
0036     @staticmethod
0037     def num_encode(number, length=4):
0038         """make a short unique ascii string out of number, truncate to length"""
0039         result = []
0040         while number and len(result) < length:
0041             number, remainder = divmod(number, Fmt.base)
0042             result.append(Fmt.alphabet[remainder])
0043         return ''.join(reversed(result))
0044 
0045     def get_value(self, key, args, kwargs):
0046         if key.startswith('id(') and key.endswith(')'):
0047             idpar = key[3:-1]
0048             if idpar == 'self':
0049                 idpar = 'SELF'
0050             if kwargs[idpar] is None:
0051                 return 'None'
0052             if Debug.neutral:
0053                 return '....'
0054             return Fmt.num_encode(id(kwargs[idpar]))
0055         if key == 'self':
0056             return kwargs['SELF']
0057         return kwargs[key]
0058 
0059 Fmt.formatter = Fmt()
0060 
0061 def id4(obj):
0062     """object id for debug messages"""
0063     if obj is None:
0064         return 'NONE'
0065     if hasattr(obj, 'uid'):
0066         return obj.uid
0067     return '.' if Debug.neutral else Fmt.num_encode(id(obj))
0068 
0069 def fmt(text, **kwargs):
0070     """use the context dict for finding arguments.
0071     For something like {self} output 'self:selfValue'"""
0072     if '}' in text:
0073         parts = []
0074         for part in text.split('}'):
0075             if '{' not in part:
0076                 parts.append(part)
0077             else:
0078                 part2 = part.split('{')
0079                 if part2[1] == 'callers':
0080                     if part2[0]:
0081                         parts.append('%s:{%s}' % (part2[0], part2[1]))
0082                     else:
0083                         parts.append('{%s}' % part2[1])
0084                 else:
0085                     showName = part2[1] + ':'
0086                     if showName.startswith('_hide'):
0087                         showName = ''
0088                     if showName.startswith('self.'):
0089                         showName = showName[5:]
0090                     parts.append('%s%s{%s}' % (part2[0], showName, part2[1]))
0091         text = ''.join(parts)
0092     argdict = _getframe(1).f_locals
0093     argdict.update(kwargs)
0094     if 'self' in argdict:
0095         # formatter.format will not accept 'self' as keyword
0096         argdict['SELF'] = argdict['self']
0097         del argdict['self']
0098     return Fmt.formatter.format(text, **argdict)
0099 
0100 
0101 def translateServerMessage(msg):
0102     """because a PB exception can not pass a list of arguments, the server
0103     encodes them into one string using SERVERMARK as separator. That
0104     string is always english. Here we unpack and translate it into the
0105     client language."""
0106     if msg.find(SERVERMARK) >= 0:
0107         return i18n(*tuple(msg.split(SERVERMARK)[1:-1]))
0108     return msg
0109 
0110 
0111 def dbgIndent(this, parent):
0112     """show messages indented"""
0113     if this.indent == 0:
0114         return ''
0115     pIndent = parent.indent if parent else 0
0116     return (' │ ' * (pIndent)) + ' ├' + '─' * (this.indent - pIndent - 1)
0117 
0118 
0119 def __logUnicodeMessage(prio, msg):
0120     """if we can encode the str msg to ascii, do so.
0121     Otherwise convert the str object into an utf-8 encoded
0122     str object.
0123     The logger module would log the str object with the
0124     marker feff at the beginning of every message, we do not want that."""
0125     msg = msg.encode(getpreferredencoding(), 'ignore')[:4000]
0126     msg = msg.decode(getpreferredencoding())
0127     Internal.logger.log(prio, msg)
0128 
0129 
0130 def __enrichMessage(msg, withGamePrefix=True):
0131     """
0132     Add some optional prefixes to msg: S/C, process id, time, git commit.
0133 
0134     @param msg: The original message.
0135     @type msg: C{str}
0136     @param withGamePrefix: If set, prepend the game prefix.
0137     @type withGamePrefix: C{Boolean}
0138     @rtype: C{str}
0139     """
0140     result = msg  # set the default
0141     if withGamePrefix and Internal.logPrefix:
0142         result = '{prefix}{process}: {msg}'.format(
0143             prefix=Internal.logPrefix,
0144             process=os.getpid() if Debug.process else '',
0145             msg=msg)
0146     if Debug.time:
0147         result = '{:08.4f} {}'.format(elapsedSince(Debug.time), result)
0148     if Debug.git:
0149         head = gitHead()
0150         if head not in ('current', None):
0151             result = 'git:{}/p3 {}'.format(head, result)
0152     if int(Debug.callers):
0153         result = '  ' + result
0154     return result
0155 
0156 
0157 def __exceptionToString(exception):
0158     """
0159     Convert exception into a useful string for logging.
0160 
0161     @param exception: The exception to be logged.
0162     @type exception: C{Exception}
0163 
0164     @rtype: C{str}
0165     """
0166     parts = []
0167     for arg in exception.args:
0168         if hasattr(arg, 'strerror'):
0169             # when using py kde 4, this is already translated at this point
0170             # but I do not know what it does differently with gettext and if
0171             # I can do the same with the python gettext module
0172             parts.append(
0173                 '[Errno {}] {}'.format(arg.errno, i18n(arg.strerror)))
0174         elif arg is None:
0175             pass
0176         else:
0177             parts.append(str(arg))
0178     if hasattr(exception, 'filename'):
0179         parts.append(exception.filename)
0180     return ' '.join(parts)
0181 
0182 
0183 def logMessage(msg, prio, showDialog, showStack=False, withGamePrefix=True):
0184     """writes info message to log and to stdout"""
0185     # pylint: disable=R0912
0186     if isinstance(msg, Exception):
0187         msg = __exceptionToString(msg)
0188     msg = str(msg)
0189     msg = translateServerMessage(msg)
0190     __logUnicodeMessage(prio, __enrichMessage(msg, withGamePrefix))
0191     if showStack:
0192         if showStack is True:
0193             lower = 2
0194         else:
0195             lower = -showStack - 3
0196         for line in traceback.format_stack()[lower:-3]:
0197             if 'logException' not in line:
0198                 __logUnicodeMessage(prio, '  ' + line.strip())
0199     if int(Debug.callers):
0200         __logUnicodeMessage(prio, callers(int(Debug.callers)))
0201     if showDialog and not Internal.isServer:
0202         return Information(msg) if prio == logging.INFO else Sorry(msg, always=True)
0203     return NoPrompt(msg)
0204 
0205 
0206 def logInfo(msg, showDialog=False, withGamePrefix=True):
0207     """log an info message"""
0208     return logMessage(msg, logging.INFO, showDialog, withGamePrefix=withGamePrefix)
0209 
0210 
0211 def logError(msg, showStack=True, withGamePrefix=True):
0212     """log an error message"""
0213     return logMessage(msg, logging.ERROR, True, showStack=showStack, withGamePrefix=withGamePrefix)
0214 
0215 
0216 def logDebug(msg, showStack=False, withGamePrefix=True, btIndent=None):
0217     """log this message and show it on stdout
0218     if btIndent is set, message is indented by depth(backtrace)-btIndent"""
0219     if btIndent:
0220         depth = traceback.extract_stack()
0221         msg = ' ' * (len(depth) - btIndent) + msg
0222     return logMessage(msg, logging.DEBUG, False, showStack=showStack, withGamePrefix=withGamePrefix)
0223 
0224 
0225 def logWarning(msg, withGamePrefix=True):
0226     """log this message and show it on stdout"""
0227     return logMessage(msg, logging.WARNING, True, withGamePrefix=withGamePrefix)
0228 
0229 
0230 def logException(exception: str, withGamePrefix=True):
0231     """logs error message and re-raises exception"""
0232     logError(exception, withGamePrefix=withGamePrefix)
0233     raise Exception(exception)
0234 
0235 
0236 class EventData(str):
0237 
0238     """used for generating a nice string"""
0239     events = {y: x for x, y in QEvent.__dict__.items() if isinstance(y, int)}
0240     # those are not documented for qevent but appear in Qt5Core/qcoreevent.h
0241     extra = {
0242         15: 'Create',
0243         16: 'Destroy',
0244         20: 'Quit',
0245         152: 'AcceptDropsChange',
0246         154: 'Windows:ZeroTimer'
0247     }
0248     events.update(extra)
0249     keys = {y: x for x, y in Qt.__dict__.items() if isinstance(y, int)}
0250 
0251     def __new__(cls, receiver, event, prefix=None):
0252         """create the wanted string"""
0253         # pylint: disable=too-many-branches
0254         if event.type() in cls.events:
0255             # ignore unknown event types
0256             name = cls.events[event.type()]
0257             value = ''
0258             if hasattr(event, 'key'):
0259                 if event.key() in cls.keys:
0260                     value = cls.keys[event.key()]
0261                 else:
0262                     value = 'unknown key:%s' % event.key()
0263             if hasattr(event, 'text'):
0264                 eventText = str(event.text())
0265                 if eventText and eventText != '\r':
0266                     value += ':%s' % eventText
0267             if value:
0268                 value = '(%s)' % value
0269             msg = '%s%s->%s' % (name, value, receiver)
0270             if hasattr(receiver, 'text'):
0271                 if receiver.__class__.__name__ != 'QAbstractSpinBox':
0272                     # accessing QAbstractSpinBox.text() gives a segfault
0273                     try:
0274                         msg += '(%s)' % receiver.text()
0275                     except TypeError:
0276                         msg += '(%s)' % receiver.text
0277             elif hasattr(receiver, 'objectName'):
0278                 msg += '(%s)' % receiver.objectName()
0279         else:
0280             msg = 'unknown event:%s' % event.type()
0281         if prefix:
0282             msg = ': '.join([prefix, msg])
0283         if 'all' in Debug.events or any(x in msg for x in Debug.events.split(':')):
0284             logDebug(msg)
0285         return msg