Completed
Pull Request — master (#34)
by
unknown
01:26
created

src.pytest_benchmark.parse_columns()   A

Complexity

Conditions 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
dl 0
loc 11
rs 9.4286
1
from __future__ import division
2
from __future__ import print_function
3
4
import argparse
5
import json
6
import os
7
import platform
8
import re
9
import subprocess
10
import sys
11
import types
12
from datetime import datetime
13
from decimal import Decimal
14
15
from .compat import PY3
16
17
try:
18
    from subprocess import check_output
19
except ImportError:
20
    def check_output(*popenargs, **kwargs):
21
        if 'stdout' in kwargs:
22
            raise ValueError('stdout argument not allowed, it will be overridden.')
23
        process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
24
        output, unused_err = process.communicate()
25
        retcode = process.poll()
26
        if retcode:
27
            cmd = kwargs.get("args")
28
            if cmd is None:
29
                cmd = popenargs[0]
30
            raise subprocess.CalledProcessError(retcode, cmd)
31
        return output
32
33
34
class SecondsDecimal(Decimal):
35
    def __float__(self):
36
        return float(super(SecondsDecimal, self).__str__())
37
38
    def __str__(self):
39
        return "{0}s".format(format_time(float(super(SecondsDecimal, self).__str__())))
40
41
    @property
42
    def as_string(self):
43
        return super(SecondsDecimal, self).__str__()
44
45
46
class NameWrapper(object):
47
    def __init__(self, target):
48
        self.target = target
49
50
    def __str__(self):
51
        name = self.target.__module__ + "." if hasattr(self.target, '__module__') else ""
52
        name += self.target.__name__ if hasattr(self.target, '__name__') else repr(self.target)
53
        return name
54
55
    def __repr__(self):
56
        return "NameWrapper(%s)" % repr(self.target)
57
58
59
def get_tag():
60
    info = get_commit_info()
61
    return '%s_%s%s' % (info['id'], get_current_time(), '_uncommitted-changes' if info['dirty'] else '')
62
63
64
def get_commit_info():
65
    dirty = False
66
    commit = 'unversioned'
67
    try:
68
        if os.path.exists('.git'):
69
            desc = check_output('git describe --dirty --always --long --abbrev=40'.split(),
70
                                universal_newlines=True).strip()
71
            desc = desc.split('-')
72
            if desc[-1].strip() == 'dirty':
73
                dirty = True
74
                desc.pop()
75
            commit = desc[-1].strip('g')
76
        elif os.path.exists('.hg'):
77
            desc = check_output('hg id --id --debug'.split(), universal_newlines=True).strip()
78
            if desc[-1] == '+':
79
                dirty = True
80
            commit = desc.strip('+')
81
        return {
82
            'id': commit,
83
            'dirty': dirty
84
        }
85
    except Exception as exc:
86
        return {
87
            'id': 'unknown',
88
            'dirty': dirty,
89
            'error': repr(exc),
90
        }
91
92
93
def get_current_time():
94
    return datetime.now().strftime("%Y%m%d_%H%M%S")
95
96
97
def first_or_value(obj, value):
98
    if obj:
99
        value, = obj
100
101
    return value
102
103
104
def load_timer(string):
105
    if "." not in string:
106
        raise argparse.ArgumentTypeError("Value for --benchmark-timer must be in dotted form. Eg: 'module.attr'.")
107
    mod, attr = string.rsplit(".", 1)
108
    if mod == 'pep418':
109
        if PY3:
110
            import time
111
            return NameWrapper(getattr(time, attr))
112
        else:
113
            from . import pep418
114
            return NameWrapper(getattr(pep418, attr))
115
    else:
116
        __import__(mod)
117
        mod = sys.modules[mod]
118
        return NameWrapper(getattr(mod, attr))
119
120
121
class RegressionCheck(object):
122
    def __init__(self, field, threshold):
123
        self.field = field
124
        self.threshold = threshold
125
126
    def fails(self, current, compared):
127
        val = self.compute(current, compared)
128
        if val > self.threshold:
129
            return "Field %s has failed %s: %.9f > %.9f" % (
130
                self.field, self.__class__.__name__, val, self.threshold
131
            )
132
133
134
class PercentageRegressionCheck(RegressionCheck):
135
    def compute(self, current, compared):
136
        val = compared[self.field]
137
        if not val:
138
            return float("inf")
139
        return current[self.field] / val * 100 - 100
140
141
142
class DifferenceRegressionCheck(RegressionCheck):
143
    def compute(self, current, compared):
144
        return current[self.field] - compared[self.field]
145
146
147
def parse_compare_fail(string,
148
                       rex=re.compile('^(?P<field>min|max|mean|median|stddev|iqr):'
149
                                      '((?P<percentage>[0-9]?[0-9])%|(?P<difference>[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?))$')):
150
    m = rex.match(string)
151
    if m:
152
        g = m.groupdict()
153
        if g['percentage']:
154
            return PercentageRegressionCheck(g['field'], int(g['percentage']))
155
        elif g['difference']:
156
            return DifferenceRegressionCheck(g['field'], float(g['difference']))
157
158
    raise argparse.ArgumentTypeError("Could not parse value: %r." % string)
159
160
161
def parse_warmup(string):
162
    string = string.lower().strip()
163
    if string == "auto":
164
        return platform.python_implementation() == "PyPy"
165
    elif string in ["off", "false", "no"]:
166
        return False
167
    elif string in ["on", "true", "yes", ""]:
168
        return True
169
    else:
170
        raise argparse.ArgumentTypeError("Could not parse value: %r." % string)
171
172
173
def parse_timer(string):
174
    return str(load_timer(string))
175
176
177
def parse_sort(string):
178
    string = string.lower().strip()
179
    if string not in ("min", "max", "mean", "stddev"):
180
        raise argparse.ArgumentTypeError(
181
            "Unacceptable value: %r. "
182
            "Value for --benchmark-sort must be one of: 'min', 'max', 'mean' or 'stddev'." % string)
183
    return string
184
185
def parse_columns(string):
186
    allowed_columns = ["min", "max", "mean", "stddev", "median", "iqr",
187
                       "outliers", "rounds", "iterations"]
188
    columns = map(str.strip, string.lower().split(','))
189
    invalid = set(columns) - set(allowed_columns)
190
    if invalid:
191
        # there are extra items in columns!
192
        msg = "Invalid column name(s): %s. " % ', '.join(invalid)
193
        msg += "The only valid column names are: %s" % ', '.join(allowed_columns)
194
        raise argparse.ArgumentTypeError(msg)
195
    return columns
196
197
def parse_rounds(string):
198
    try:
199
        value = int(string)
200
    except ValueError as exc:
201
        raise argparse.ArgumentTypeError(exc)
202
    else:
203
        if value < 1:
204
            raise argparse.ArgumentTypeError("Value for --benchmark-rounds must be at least 1.")
205
        return value
206
207
208
def parse_seconds(string):
209
    try:
210
        return SecondsDecimal(string).as_string
211
    except Exception as exc:
212
        raise argparse.ArgumentTypeError("Invalid decimal value %r: %r" % (string, exc))
213
214
215
def parse_save(string):
216
    if not string:
217
        raise argparse.ArgumentTypeError("Can't be empty.")
218
    illegal = ''.join(c for c in r"\/:*?<>|" if c in string)
219
    if illegal:
220
        raise argparse.ArgumentTypeError("Must not contain any of these characters: /:*?<>|\\ (it has %r)" % illegal)
221
    return string
222
223
224
def time_unit(value):
225
    if value < 1e-6:
226
        return "n", 1e9
227
    elif value < 1e-3:
228
        return "u", 1e6
229
    elif value < 1:
230
        return "m", 1e3
231
    else:
232
        return "", 1.
233
234
235
def format_time(value):
236
    unit, adjustment = time_unit(value)
237
    return "{0:.2f}{1:s}".format(value * adjustment, unit)
238
239
240
class cached_property(object):
241
    def __init__(self, func):
242
        self.__doc__ = getattr(func, '__doc__')
243
        self.func = func
244
245
    def __get__(self, obj, cls):
246
        if obj is None:
247
            return self
248
        value = obj.__dict__[self.func.__name__] = self.func(obj)
249
        return value
250
251
252
def clonefunc(f):
253
    """Deep clone the given function to create a new one.
254
255
    By default, the PyPy JIT specializes the assembler based on f.__code__:
256
    clonefunc makes sure that you will get a new function with a **different**
257
    __code__, so that PyPy will produce independent assembler. This is useful
258
    e.g. for benchmarks and microbenchmarks, so you can make sure to compare
259
    apples to apples.
260
261
    Use it with caution: if abused, this might easily produce an explosion of
262
    produced assembler.
263
264
    from: https://bitbucket.org/antocuni/pypytools/src/tip/pypytools/util.py?at=default
265
    """
266
267
    # first of all, we clone the code object
268
    try:
269
        co = f.__code__
270
        if PY3:
271
            co2 = types.CodeType(co.co_argcount, co.co_kwonlyargcount,
272
                                 co.co_nlocals, co.co_stacksize, co.co_flags, co.co_code,
273
                                 co.co_consts, co.co_names, co.co_varnames, co.co_filename, co.co_name,
274
                                 co.co_firstlineno, co.co_lnotab, co.co_freevars, co.co_cellvars)
275
        else:
276
            co2 = types.CodeType(co.co_argcount, co.co_nlocals, co.co_stacksize, co.co_flags, co.co_code,
277
                                 co.co_consts, co.co_names, co.co_varnames, co.co_filename, co.co_name,
278
                                 co.co_firstlineno, co.co_lnotab, co.co_freevars, co.co_cellvars)
279
        #
280
        # then, we clone the function itself, using the new co2
281
        return types.FunctionType(co2, f.__globals__, f.__name__, f.__defaults__, f.__closure__)
282
    except AttributeError:
283
        return f
284
285
286
def format_dict(obj):
287
    return "{%s}" % ", ".join("%s: %s" % (k, json.dumps(v)) for k, v in sorted(obj.items()))
288
289
290
def report_progress(iterable, terminal_reporter, format_string, **kwargs):
291
    total = len(iterable)
292
293
    def progress_reporting_wrapper():
294
        for pos, item in enumerate(iterable):
295
            string = format_string.format(pos=pos + 1, total=total, value=item, **kwargs)
296
            terminal_reporter.rewrite(string, black=True, bold=True)
297
            yield string, item
298
    return progress_reporting_wrapper()
299