util.py 8.13 KB
Newer Older
1
2
3
# -*- coding: utf-8 -*-

"""
4
Copyright (C) 2008-2016 Wolfgang Rohdewald <wolfgang@rohdewald.de>
5

6
Kajongg is free software you can redistribute it and/or modify
7
8
9
10
11
12
13
14
15
16
17
it under the terms of the GNU General Public License as published by
the Free Software Foundation either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program if not, write to the Free Software
18
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
20
"""

21
22
23
# util must not import from log because util should not
# depend on kde.py

24
from __future__ import print_function
25
26
27
import traceback
import os
import datetime
28
import subprocess
29
import gc
30
31
import csv

32

33
from locale import getpreferredencoding
34
from sys import stdout
35

36
from common import Debug
37

38
39
40
41
try:
    STDOUTENCODING = stdout.encoding
except AttributeError:
    STDOUTENCODING = None
42
if not STDOUTENCODING:
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
43
    STDOUTENCODING = getpreferredencoding()
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
44

45

46
def stack(msg, limit=6):
47
48
    """returns a list of lines with msg as prefix"""
    result = []
49
50
    for idx, values in enumerate(
            traceback.extract_stack(limit=limit + 2)[:-2]):
51
        fileName, line, function, txt = values
52
53
54
55
56
        result.append(
            '%2d: %s %s/%d %s: %s' %
            (idx, msg, os.path.splitext(
                os.path.basename(fileName))[0],
             line, function, txt))
57
58
    return result

59

60
def callers(count=5, exclude=None):
61
62
    """returns the name of the calling method"""
    stck = traceback.extract_stack(limit=30)
63
    excluding = list(exclude) if exclude else []
64
    excluding.extend(['<genexpr>', '__call__', 'run', '<module>', 'runTests'])
65
66
67
    excluding.extend(['_startRunCallbacks', '_runCallbacks', 'remote_move', 'exec_move'])
    excluding.extend(['proto_message', '_recvMessage', 'remoteMessageReceived'])
    excluding.extend(['clientAction', 'myAction', 'expressionReceived'])
68
    excluding.extend(['_read', 'callWithLogger'])
69
70
71
72
    excluding.extend(['callbackIfDone', 'callback', '__gotAnswer'])
    excluding.extend(['callExpressionReceived', 'proto_answer'])
    excluding.extend(['_dataReceived', 'dataReceived', 'gotItem'])
    excluding.extend(['callWithContext', '_doReadOrWrite', 'doRead'])
73
    excluding.extend(['callers', 'debug', 'logMessage', 'logDebug'])
74
75
    _ = list(([x[2] for x in stck if x[2] not in excluding]))
    names = reversed(_[-count:])
76
    result = '.'.join(names)
77
    return '[{}]'.format(result)
78

79

80
def elapsedSince(since):
81
82
    """returns seconds since since"""
    delta = datetime.datetime.now() - since
83
84
85
86
    return float(
        delta.microseconds
        + (delta.seconds + delta.days * 24 * 3600) * 10 ** 6) / 10 ** 6

87

88
89
def which(program):
    """returns the full path for the binary or None"""
90
    for path in os.environ['PATH'].split(os.pathsep):
91
92
93
        fullName = os.path.join(path, program)
        if os.path.exists(fullName):
            return fullName
94

95

Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
96
97
98
99
100
101
102
def removeIfExists(filename):
    """remove file if it exists. Returns True if it existed"""
    exists = os.path.exists(filename)
    if exists:
        os.remove(filename)
    return exists

103

104
105
106
107
def uniqueList(seq):
    """makes list content unique, keeping only the first occurrence"""
    seen = set()
    seen_add = seen.add
108
    return [x for x in seq if x not in seen and not seen_add(x)]
109

110

111
112
113
def _getr(slist, olist, seen):
    """Recursively expand slist's objects into olist, using seen to track
    already processed objects."""
Yuri Chornoivan's avatar
Yuri Chornoivan committed
114
115
    for element in slist:
        if id(element) in seen:
116
            continue
Yuri Chornoivan's avatar
Yuri Chornoivan committed
117
118
119
        seen[id(element)] = None
        olist.append(element)
        tlist = gc.get_referents(element)
120
121
122
123
        if tlist:
            _getr(tlist, olist, seen)

# The public function.
124
125


126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def get_all_objects():
    """Return a list of all live Python objects, not including the
    list itself. May use this in Duration for showing where
    objects are leaking"""
    gc.collect()
    gcl = gc.get_objects()
    olist = []
    seen = {}
    # Just in case:
    seen[id(gcl)] = None
    seen[id(olist)] = None
    seen[id(seen)] = None
    # _getr does the real work.
    _getr(gcl, olist, seen)
    return olist

142

143
class Duration:
144

145
    """a helper class for checking code execution duration"""
146

147
    def __init__(self, name, threshold=None, bug=False):
148
        """name describes where in the source we are checking
149
150
        threshold in seconds: do not warn below
        if bug is True, throw an exception if threshold is exceeded"""
151
        self.name = name
152
        self.threshold = threshold or 1.0
153
154
155
156
157
158
        self.bug = bug
        self.__start = datetime.datetime.now()

    def __enter__(self):
        return self

Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
159
    def __exit__(self, exc_type, exc_value, trback):
160
        """now check time passed"""
161
162
163
        if not Debug.neutral:
            diff = datetime.datetime.now() - self.__start
            if diff > datetime.timedelta(seconds=self.threshold):
164
165
166
167
                msg = '%s took %d.%02d seconds' % (
                    self.name,
                    diff.seconds,
                    diff.microseconds)
168
169
170
171
                if self.bug:
                    raise UserWarning(msg)
                else:
                    print(msg)
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
172

173

Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
174
175
def checkMemory():
    """as the name says"""
176
    # pylint: disable=too-many-branches
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
177
178
    if not Debug.gc:
        return
179
180
    gc.set_threshold(0)
    gc.set_debug(gc.DEBUG_LEAK)
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
181
    gc.enable()
182
    print('collecting {{{')
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
183
    gc.collect()        # we want to eliminate all output
184
    print('}}} done')
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
185
186

    # code like this may help to find specific things
187
    if True: # pylint: disable=using-constant-test
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
188
189
190
191
192
193
194
195
196
197
198
199
200
        interesting = ('Client', 'Player', 'Game')
        for obj in gc.garbage:
            if hasattr(obj, 'cell_contents'):
                obj = obj.cell_contents
            if not any(x in repr(obj) for x in interesting):
                continue
            for referrer in gc.get_referrers(obj):
                if referrer is gc.garbage:
                    continue
                if hasattr(referrer, 'cell_contents'):
                    referrer = referrer.cell_contents
                if referrer.__class__.__name__ in interesting:
                    for referent in gc.get_referents(referrer):
201
                        print('%s refers to %s' % (referrer, referent))
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
202
                else:
203
204
205
                    print('referrer of %s/%s is: id=%s type=%s %s' %
                          (type(obj), obj, id(referrer),
                           type(referrer), referrer))
206
    print('unreachable:%s' % gc.collect())
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
207
    gc.set_debug(0)
208

209

210
def gitHead():
211
212
    """the current git commit. 'current' if there are uncommitted changes
    and None if no .git found"""
213
    if not os.path.exists(os.path.join('..', '.git')):
214
        return None
215
    subprocess.Popen(['git', 'update-index', '-q', '--refresh'])
216
    uncommitted = list(popenReadlines('git diff-index --name-only HEAD --'))
217
    return 'current' if uncommitted else next(popenReadlines('git log -1 --format=%h'))
218

219

220
class CsvWriter:
221
222
223
224
225
226
227
    """hide differences between Python 2 and 3"""
    def __init__(self, filename, mode='w'):
        self.outfile = open(filename, mode)
        self.__writer = csv.writer(self.outfile, delimiter=Csv.delimiter)

    def writerow(self, row):
        """write one row"""
Wolfgang Rohdewald's avatar
Wolfgang Rohdewald committed
228
        self.__writer.writerow(list(str(cell) for cell in row))
229
230
231
232
233

    def __del__(self):
        """clean up"""
        self.outfile.close()

234
class Csv:
235
236
237
238
239
240
241
242
    """hide differences between Python 2 and 3"""

    delimiter = ';'

    @staticmethod
    def reader(filename):
        """returns a generator for decoded strings"""
        return csv.reader(open(filename, 'r', encoding='utf-8'), delimiter=Csv.delimiter)
243
244
245

def popenReadlines(args):
    """runs a subprocess and returns stdout as a list of unicode encodes lines"""
246
247
    if isinstance(args, str):
        args = args.split()
248
249
250
    my_env = os.environ.copy()
    my_env["LANG"] = "C"
    result = subprocess.Popen(args, universal_newlines=True, stdout=subprocess.PIPE, env=my_env).communicate()[0]
251
    return (x.strip() for x in result.split('\n') if x.strip())