perftester 0.4.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- perftester/perftester.py +19 -4
- perftester/tmp.py +69 -0
- perftester/tmp_working.py +34 -0
- perftester/understand.py +476 -0
- {perftester-0.4.0.dist-info → perftester-0.5.1.dist-info}/METADATA +8 -8
- perftester-0.5.1.dist-info/RECORD +12 -0
- perftester-0.4.0.dist-info/RECORD +0 -9
- {perftester-0.4.0.dist-info → perftester-0.5.1.dist-info}/LICENSE +0 -0
- {perftester-0.4.0.dist-info → perftester-0.5.1.dist-info}/WHEEL +0 -0
- {perftester-0.4.0.dist-info → perftester-0.5.1.dist-info}/entry_points.txt +0 -0
- {perftester-0.4.0.dist-info → perftester-0.5.1.dist-info}/top_level.txt +0 -0
perftester/perftester.py
CHANGED
|
@@ -285,7 +285,7 @@ class Config:
|
|
|
285
285
|
]
|
|
286
286
|
self.memory_benchmark = min(max(r) for r in memory_results)
|
|
287
287
|
|
|
288
|
-
def set_defaults(self, which, number=None, repeat=None):
|
|
288
|
+
def set_defaults(self, which, number=None, repeat=None, Number=None, Repeat=None):
|
|
289
289
|
"""Change the default settings.
|
|
290
290
|
|
|
291
291
|
Beware! This does not change particular settings for a particular test,
|
|
@@ -302,6 +302,11 @@ class Config:
|
|
|
302
302
|
repeat (int, optional): passed to timeit.repeat as repeat.
|
|
303
303
|
Defaults to None.
|
|
304
304
|
"""
|
|
305
|
+
if number is None and Number is not None:
|
|
306
|
+
number = Number
|
|
307
|
+
if repeat is None and Repeat is not None:
|
|
308
|
+
repeat = Repeat
|
|
309
|
+
|
|
305
310
|
self._check_args(lambda: 0, which, number, repeat)
|
|
306
311
|
|
|
307
312
|
if number is not None:
|
|
@@ -309,7 +314,7 @@ class Config:
|
|
|
309
314
|
if repeat is not None:
|
|
310
315
|
self.defaults[which]["repeat"] = repeat
|
|
311
316
|
|
|
312
|
-
def set(self, func, which, number=None, repeat=None):
|
|
317
|
+
def set(self, func, which, number=None, repeat=None, Number=None, Repeat=None):
|
|
313
318
|
"""Set a particular argument.
|
|
314
319
|
|
|
315
320
|
Args:
|
|
@@ -322,6 +327,11 @@ class Config:
|
|
|
322
327
|
as repeat; for memory tests, the number of runs of the test.
|
|
323
328
|
Defaults to None.
|
|
324
329
|
"""
|
|
330
|
+
if number is None and Number is not None:
|
|
331
|
+
number = Number
|
|
332
|
+
if repeat is None and Repeat is not None:
|
|
333
|
+
repeat = Repeat
|
|
334
|
+
|
|
325
335
|
self._check_args(func, which, number, repeat)
|
|
326
336
|
|
|
327
337
|
if func not in self.settings.keys():
|
|
@@ -371,6 +381,12 @@ class Config:
|
|
|
371
381
|
"For memory tests, you can only set repeat, not number.",
|
|
372
382
|
)
|
|
373
383
|
|
|
384
|
+
if number is not None:
|
|
385
|
+
if int(number) == number:
|
|
386
|
+
number = int(number)
|
|
387
|
+
if repeat is not None:
|
|
388
|
+
if int(repeat) == repeat:
|
|
389
|
+
repeat = int(repeat)
|
|
374
390
|
check_instance(
|
|
375
391
|
number,
|
|
376
392
|
(int, None),
|
|
@@ -380,7 +396,6 @@ class Config:
|
|
|
380
396
|
f"{type(number).__name__}"
|
|
381
397
|
),
|
|
382
398
|
)
|
|
383
|
-
|
|
384
399
|
check_instance(
|
|
385
400
|
repeat,
|
|
386
401
|
(int, None),
|
|
@@ -805,7 +820,7 @@ def pp(*args):
|
|
|
805
820
|
0.1222
|
|
806
821
|
>>> pp(dict(a=.12121212, b=23.234234234), ["system failure", 345345.345])
|
|
807
822
|
{'a': 0.1212, 'b': 23.23}
|
|
808
|
-
['system failure', 345300]
|
|
823
|
+
['system failure', 345300.0]
|
|
809
824
|
"""
|
|
810
825
|
for arg in args:
|
|
811
826
|
pprint(
|
perftester/tmp.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from pprint import pprint
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Container:
|
|
5
|
+
def __init__(self, values):
|
|
6
|
+
self.values = values
|
|
7
|
+
|
|
8
|
+
def __gt__(self, other):
|
|
9
|
+
return Container(type(self.values)(other(i) for i in self.values))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Show:
|
|
13
|
+
def __call__(self, obj=None, func=None, *args, **kwargs):
|
|
14
|
+
self.obj = obj
|
|
15
|
+
if not func:
|
|
16
|
+
pprint(obj.result)
|
|
17
|
+
else:
|
|
18
|
+
pprint(func(obj.result, *args, **kwargs))
|
|
19
|
+
return obj
|
|
20
|
+
|
|
21
|
+
def __gt__(self, other):
|
|
22
|
+
return other(self.obj.result)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Value:
|
|
27
|
+
def __init__(self, value):
|
|
28
|
+
self.result = value
|
|
29
|
+
|
|
30
|
+
def __call__(self):
|
|
31
|
+
return self.result
|
|
32
|
+
|
|
33
|
+
def __gt__(self, other):
|
|
34
|
+
return other(self.result)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Do:
|
|
38
|
+
def __init__(self, func, *args, parallel=False, **kwargs):
|
|
39
|
+
self.func = func
|
|
40
|
+
self.args = args
|
|
41
|
+
self.kwargs = kwargs
|
|
42
|
+
self.result = None
|
|
43
|
+
self.parallel = parallel
|
|
44
|
+
|
|
45
|
+
def __call__(self, *add_args, **add_kwargs):
|
|
46
|
+
these_args = {**self.kwargs, **add_kwargs}
|
|
47
|
+
self.result = self.func(*add_args, *self.args, **these_args)
|
|
48
|
+
return self.result
|
|
49
|
+
|
|
50
|
+
def __gt__(self, other):
|
|
51
|
+
if isinstance(other, Show):
|
|
52
|
+
return other(self)
|
|
53
|
+
try:
|
|
54
|
+
return other(self.__call__())
|
|
55
|
+
except:
|
|
56
|
+
return other(self.result)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
double = lambda x: 2 * x
|
|
61
|
+
square = lambda x: x ** 2
|
|
62
|
+
add = lambda x, y: x + y
|
|
63
|
+
|
|
64
|
+
Value(10) > Do(double) > Show() > Do(double) > Show()
|
|
65
|
+
|
|
66
|
+
Value(10) > Do(double) > Do(square) > Do(add, 50) > Do(print)
|
|
67
|
+
Value(10) > Do(double) > Do(square) > Show() > Do(add, 50) > Show()
|
|
68
|
+
|
|
69
|
+
Container([1, 2, 3]) > Do(double) > Show()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class Value:
|
|
2
|
+
def __init__(self, value):
|
|
3
|
+
self.result = value
|
|
4
|
+
|
|
5
|
+
def __call__(self):
|
|
6
|
+
return self.result
|
|
7
|
+
|
|
8
|
+
def __gt__(self, other):
|
|
9
|
+
return other(self.result)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Do:
|
|
13
|
+
def __init__(self, func=None, *args, **kwargs):
|
|
14
|
+
self.func = func
|
|
15
|
+
self.args = args
|
|
16
|
+
self.kwargs = kwargs
|
|
17
|
+
|
|
18
|
+
def __call__(self, *add_args, **add_kwargs):
|
|
19
|
+
these_args = {**self.kwargs, **add_kwargs}
|
|
20
|
+
self.result = self.func(*add_args, *self.args, **these_args)
|
|
21
|
+
return self.result
|
|
22
|
+
|
|
23
|
+
def __gt__(self, other):
|
|
24
|
+
try:
|
|
25
|
+
return other(self.__call__())
|
|
26
|
+
except:
|
|
27
|
+
return other(self.result)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
double = lambda x: 2 * x
|
|
31
|
+
square = lambda x: x ** 2
|
|
32
|
+
add = lambda x, y: x + y
|
|
33
|
+
|
|
34
|
+
Value(10) > Do(double) > Do(square) > Do(add, 50) > Do(print)
|
perftester/understand.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import copy
|
|
3
|
+
import inspect
|
|
4
|
+
import json
|
|
5
|
+
import pathlib
|
|
6
|
+
import pickle
|
|
7
|
+
import rounder
|
|
8
|
+
|
|
9
|
+
import easycheck
|
|
10
|
+
|
|
11
|
+
from collections import namedtuple
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
from functools import wraps
|
|
14
|
+
from memory_profiler import memory_usage
|
|
15
|
+
from time import perf_counter
|
|
16
|
+
from typing import Callable, Iterable, Union
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Globals, used as dict fields in calls
|
|
20
|
+
|
|
21
|
+
CALLEDFROM = "called from"
|
|
22
|
+
CALLMODE = "calls"
|
|
23
|
+
INSTANCEMODE = "instances"
|
|
24
|
+
CALLS = "no of calls"
|
|
25
|
+
INSTANCES = "no of instances"
|
|
26
|
+
MEM = "memory peak"
|
|
27
|
+
TIME = "execution time"
|
|
28
|
+
TIME_ALL = "time spent inside (all)"
|
|
29
|
+
TIME_MEAN = "time spent inside (mean)"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Exceptions
|
|
33
|
+
|
|
34
|
+
class IncorrectDictRepresentationError(Exception):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Performance decorators
|
|
39
|
+
|
|
40
|
+
def time_performance(func: Callable) -> Callable:
|
|
41
|
+
"""Time performance decorator.
|
|
42
|
+
|
|
43
|
+
It is used to analyze the performance of the decorated function in terms
|
|
44
|
+
of execution time.
|
|
45
|
+
"""
|
|
46
|
+
@wraps(func)
|
|
47
|
+
def inner(*args, **kwargs):
|
|
48
|
+
_type, function = _get_type_and_name(func)
|
|
49
|
+
if _type == "class method":
|
|
50
|
+
aargs = list(args)
|
|
51
|
+
aargs[0] = "self"
|
|
52
|
+
aargs = tuple(aargs)
|
|
53
|
+
arguments = _stringify_args_kwargs(aargs, kwargs)
|
|
54
|
+
else:
|
|
55
|
+
arguments = _stringify_args_kwargs(args, kwargs)
|
|
56
|
+
|
|
57
|
+
global calls
|
|
58
|
+
|
|
59
|
+
call_type = INSTANCEMODE if _type == "class" else CALLMODE
|
|
60
|
+
call_count = INSTANCES if _type == "class" else CALLS
|
|
61
|
+
|
|
62
|
+
if not calls.registered[_type].get(function, False):
|
|
63
|
+
calls.registered[_type][function] = {}
|
|
64
|
+
calls.registered[_type][function][call_type] = {}
|
|
65
|
+
|
|
66
|
+
calls.registered[_type][function][call_count] = (
|
|
67
|
+
calls.registered[_type][function].get(call_count, 0) + 1
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
start = perf_counter()
|
|
71
|
+
ret = func(*args, **kwargs)
|
|
72
|
+
end = perf_counter()
|
|
73
|
+
|
|
74
|
+
ID = calls.registered[_type][function][call_count]
|
|
75
|
+
calls.registered[_type][function][call_type][ID] = {
|
|
76
|
+
CALLEDFROM: inspect.stack()[1][3],
|
|
77
|
+
}
|
|
78
|
+
calls.registered[_type][function][call_type][ID][TIME] = (
|
|
79
|
+
rounder.signif_object(end - start, 5)
|
|
80
|
+
)
|
|
81
|
+
calls.registered[_type][function][call_type][ID]["call"] = f"{function}({arguments})"
|
|
82
|
+
calls.registered[_type][function][TIME_ALL] = (
|
|
83
|
+
calls
|
|
84
|
+
.registered[_type][function]
|
|
85
|
+
.get(TIME_ALL, 0)
|
|
86
|
+
+ (end - start)
|
|
87
|
+
)
|
|
88
|
+
calls.registered[_type][function][TIME_MEAN] = (
|
|
89
|
+
calls.registered[_type][function][TIME_ALL] /
|
|
90
|
+
(
|
|
91
|
+
calls.registered[_type][function].get(CALLS, None)
|
|
92
|
+
or calls.registered[_type][function][INSTANCES]
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return ret
|
|
96
|
+
|
|
97
|
+
return inner
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def memory_performance(func: Callable) -> Callable:
|
|
101
|
+
"""Memory performance decorator.
|
|
102
|
+
|
|
103
|
+
It is used to analyze the performance of the decorated function in terms
|
|
104
|
+
of memory usage.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def inner(*args, **kwargs):
|
|
108
|
+
_type, function = _get_type_and_name(func)
|
|
109
|
+
if _type == "class method":
|
|
110
|
+
aargs = list(args)
|
|
111
|
+
aargs[0] = "self"
|
|
112
|
+
aargs = tuple(aargs)
|
|
113
|
+
arguments = _stringify_args_kwargs(aargs, kwargs)
|
|
114
|
+
else:
|
|
115
|
+
arguments = _stringify_args_kwargs(args, kwargs)
|
|
116
|
+
|
|
117
|
+
global calls
|
|
118
|
+
|
|
119
|
+
call_type = INSTANCEMODE if _type == "class" else CALLMODE
|
|
120
|
+
call_count = INSTANCES if _type == "class" else CALLS
|
|
121
|
+
|
|
122
|
+
if not calls.registered[_type].get(function, False):
|
|
123
|
+
calls.registered[_type][function] = {}
|
|
124
|
+
calls.registered[_type][function][call_type] = {}
|
|
125
|
+
|
|
126
|
+
calls.registered[_type][function][call_count] = (
|
|
127
|
+
calls.registered[_type][function].get(call_count, 0) + 1
|
|
128
|
+
)
|
|
129
|
+
memory_results, ret = memory_usage((func, args, kwargs), retval=True)
|
|
130
|
+
peak_memory = min(memory_results)
|
|
131
|
+
|
|
132
|
+
ID = calls.registered[_type][function][call_count]
|
|
133
|
+
calls.registered[_type][function][call_type][ID] = {
|
|
134
|
+
CALLEDFROM: inspect.stack()[1][3],
|
|
135
|
+
}
|
|
136
|
+
calls.registered[_type][function][call_type][ID][MEM] = (
|
|
137
|
+
peak_memory
|
|
138
|
+
)
|
|
139
|
+
calls.registered[_type][function][call_type][ID]["call"] = f"{function}({arguments})"
|
|
140
|
+
calls.registered[_type][function][MEM] = max(
|
|
141
|
+
# current max
|
|
142
|
+
calls
|
|
143
|
+
.registered[_type][function]
|
|
144
|
+
.get(MEM, 0),
|
|
145
|
+
# current memory_peak
|
|
146
|
+
peak_memory
|
|
147
|
+
)
|
|
148
|
+
return ret
|
|
149
|
+
|
|
150
|
+
return inner
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Class containing all the registered calls and class instances.
|
|
154
|
+
|
|
155
|
+
class Calls:
|
|
156
|
+
__slots__ = ("registered", "analyze", "save", "read")
|
|
157
|
+
|
|
158
|
+
def __init__(self):
|
|
159
|
+
self.registered: dict = {"class": {}, "class method": {}, "function": {}}
|
|
160
|
+
|
|
161
|
+
def show(self, digits=4, indent=4):
|
|
162
|
+
rounded_calls = rounder.signif_object(self.registered,
|
|
163
|
+
use_copy=True,
|
|
164
|
+
digits=digits)
|
|
165
|
+
print("Classes:\n", json.dumps(rounded_calls["class"], indent=indent))
|
|
166
|
+
print("Class methods:\n", json.dumps(rounded_calls["class method"], indent=indent))
|
|
167
|
+
print("Functions:\n", json.dumps(rounded_calls["function"], indent=indent))
|
|
168
|
+
|
|
169
|
+
def __repr__(self):
|
|
170
|
+
if not self.registered.keys():
|
|
171
|
+
return "No registered calls"
|
|
172
|
+
|
|
173
|
+
instances = self._get_count("class")
|
|
174
|
+
method_calls = self._get_count("class method")
|
|
175
|
+
func_calls = self._get_count("function")
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
"Registered:\n"
|
|
179
|
+
f" * {instances} class instances\n"
|
|
180
|
+
f" * {method_calls} class methods\n"
|
|
181
|
+
f" * {func_calls} function calls\n"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def _get_count(self, which):
|
|
185
|
+
calls = 0
|
|
186
|
+
if self.registered[which]:
|
|
187
|
+
for k in self.registered[which]:
|
|
188
|
+
key = INSTANCES if which == "class" else CALLS
|
|
189
|
+
calls += self.registered[which][k][key]
|
|
190
|
+
return calls
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class CallsAnalyzer:
|
|
194
|
+
"""Class to analyze Calls.registered.
|
|
195
|
+
|
|
196
|
+
This is a simple analyzer. A more adanced one will be offered
|
|
197
|
+
in a dedicated understand extension.
|
|
198
|
+
"""
|
|
199
|
+
def _update_calls(self):
|
|
200
|
+
global calls
|
|
201
|
+
self.calls = calls.registered
|
|
202
|
+
|
|
203
|
+
def summarize(self, digits=4) -> dict:
|
|
204
|
+
self._update_calls()
|
|
205
|
+
summary_calls = copy.deepcopy(self.calls)
|
|
206
|
+
for key in ("class method", "function"):
|
|
207
|
+
if summary_calls[key]:
|
|
208
|
+
for k in summary_calls[key]:
|
|
209
|
+
_ = summary_calls[key][k].pop(CALLMODE)
|
|
210
|
+
if summary_calls["class"]:
|
|
211
|
+
for k in summary_calls["class"]:
|
|
212
|
+
_ = summary_calls["class"][k].pop(INSTANCEMODE)
|
|
213
|
+
|
|
214
|
+
summary_calls = rounder.signif_object(summary_calls, digits=digits)
|
|
215
|
+
return summary_calls
|
|
216
|
+
|
|
217
|
+
summarise = summarize
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class CallsSaver:
|
|
221
|
+
def _update(self):
|
|
222
|
+
global calls
|
|
223
|
+
self.calls = calls.registered
|
|
224
|
+
|
|
225
|
+
def to_json(self, path: Union[str, pathlib.Path]):
|
|
226
|
+
self._update()
|
|
227
|
+
json_dict = json.dumps(self.calls)
|
|
228
|
+
with open(path, "w") as json_file:
|
|
229
|
+
json_file.write(json_dict)
|
|
230
|
+
|
|
231
|
+
def to_text(self, path: Union[str, pathlib.Path]):
|
|
232
|
+
self._update()
|
|
233
|
+
with open(path, "w") as text_file:
|
|
234
|
+
text_file.write(str(self.calls))
|
|
235
|
+
|
|
236
|
+
def to_pickle(self, path: Union[str, pathlib.Path]):
|
|
237
|
+
self._update()
|
|
238
|
+
with open(path, "wb") as pickle_file:
|
|
239
|
+
pickle.dump(self.calls, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class IncorrectCallsInstanceError(Exception): ...
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
CallsInstance = namedtuple("CallsInstance", "type registered")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class CallsReader:
|
|
249
|
+
__slots__ = tuple()
|
|
250
|
+
|
|
251
|
+
def _check_calls_object(self, obj, path):
|
|
252
|
+
# check if obj is a valid Calls object
|
|
253
|
+
easycheck.check_type(
|
|
254
|
+
obj,
|
|
255
|
+
expected_type=dict,
|
|
256
|
+
handle_with=IncorrectDictRepresentationError,
|
|
257
|
+
message=f"Object parsed from {path} is not a dict but"
|
|
258
|
+
f" a {type(obj).__name__}"
|
|
259
|
+
)
|
|
260
|
+
# check if the required keys are there
|
|
261
|
+
return self._get_type_of_calls(obj)
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def _get_type_of_calls(obj):
|
|
265
|
+
# check if this is a time or memory calls dict
|
|
266
|
+
# if time: return "time"
|
|
267
|
+
# if memory: return "memory"
|
|
268
|
+
# else: raise IncorrectCallsInstanceError
|
|
269
|
+
for _type in ["class", "class method", "function"]:
|
|
270
|
+
if obj[_type].keys():
|
|
271
|
+
first = list(obj[_type].keys())[0]
|
|
272
|
+
if TIME_ALL in obj[_type][first].keys():
|
|
273
|
+
return "time"
|
|
274
|
+
if MEM in obj[_type][first].keys():
|
|
275
|
+
return "memory"
|
|
276
|
+
raise IncorrectCallsInstanceError(
|
|
277
|
+
"The object is neither Time nor Memory Calls dictionary."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def from_json(self, path: Union[str, pathlib.Path]):
|
|
281
|
+
with open(path) as jsonfile:
|
|
282
|
+
registered = json.load(jsonfile)
|
|
283
|
+
|
|
284
|
+
# use int IDs (used as keys) instead of str,
|
|
285
|
+
# which is used by json.load
|
|
286
|
+
for _type in ["class", "class method", "function"]:
|
|
287
|
+
calls = "instances" if _type == "class" else "calls"
|
|
288
|
+
for method in registered[_type]:
|
|
289
|
+
registered[_type][method][calls] = {
|
|
290
|
+
int(instance): value
|
|
291
|
+
for instance, value in registered[_type][method][calls].items()
|
|
292
|
+
}
|
|
293
|
+
return CallsInstance(
|
|
294
|
+
type=self._check_calls_object(registered, path),
|
|
295
|
+
registered=registered
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def from_text(self, path: Union[str, pathlib.Path]):
|
|
299
|
+
with open(path) as f:
|
|
300
|
+
try:
|
|
301
|
+
obj_from_text = ast.literal_eval(f.read())
|
|
302
|
+
except Exception as e:
|
|
303
|
+
raise IncorrectDictRepresentationError from e
|
|
304
|
+
self._check_calls_object(obj_from_text, path)
|
|
305
|
+
return CallsInstance(
|
|
306
|
+
type=self._check_calls_object(obj_from_text, path),
|
|
307
|
+
registered=obj_from_text
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def from_pickle(self, path: Union[str, pathlib.Path]):
|
|
311
|
+
with open(path, "rb") as pickle_file:
|
|
312
|
+
pkl = pickle.load(pickle_file)
|
|
313
|
+
return CallsInstance(
|
|
314
|
+
type=self._check_calls_object(pkl, path),
|
|
315
|
+
registered=pkl
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# Helpers
|
|
320
|
+
|
|
321
|
+
def _is_class(obj):
|
|
322
|
+
return repr(obj).startswith("<class")
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _is_this_function_a_method(func):
|
|
326
|
+
return "." in func.__qualname__
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _get_type_and_name(obj):
|
|
330
|
+
if _is_class(obj):
|
|
331
|
+
return "class", f"{obj.__module__}.{obj.__name__}"
|
|
332
|
+
if hasattr(obj, "__wrapped__"):
|
|
333
|
+
name = obj.__wrapped__.__name__
|
|
334
|
+
if _is_this_function_a_method(obj):
|
|
335
|
+
name = obj.__qualname__
|
|
336
|
+
return "class method", f"{obj.__module__}.{name}"
|
|
337
|
+
return "function", f"{obj.__module__}.{obj.__name__}"
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def type_and_len_of_iterable(it, max_len=1000):
|
|
341
|
+
"""Get length of an iterable.
|
|
342
|
+
|
|
343
|
+
The iterable is copied first, and the function works
|
|
344
|
+
on the deep copy. For generators expressions, the function
|
|
345
|
+
returns "generator" as cannot get its length without emptying
|
|
346
|
+
the original generator.
|
|
347
|
+
|
|
348
|
+
If maximum_len is reached, instead of the actual length, the function
|
|
349
|
+
returns "over {max_len}", to make the function cheap.
|
|
350
|
+
"""
|
|
351
|
+
# Get length, based on copy
|
|
352
|
+
it_copy = deepcopy(it)
|
|
353
|
+
|
|
354
|
+
c = 0
|
|
355
|
+
max_len_reached = False
|
|
356
|
+
for i in it_copy:
|
|
357
|
+
c += 1
|
|
358
|
+
if i == max_len:
|
|
359
|
+
max_len_reached = True
|
|
360
|
+
break
|
|
361
|
+
|
|
362
|
+
if max_len_reached:
|
|
363
|
+
return f"{type(it).__name__} of over {max_len} items"
|
|
364
|
+
if not c:
|
|
365
|
+
return f"empty {type(it).__name__}"
|
|
366
|
+
return f"{type(it).__name__} of {c} item{'s' if c > 1 else ''}"
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _add_quotes_to_str(obj):
|
|
370
|
+
"""Add quotes to strings.
|
|
371
|
+
|
|
372
|
+
The function works recursively, so that it goes deep into the structure
|
|
373
|
+
to reach any string. If obj is "self", the quotes are not added, so that
|
|
374
|
+
in case of class methods self is printed without quotes.
|
|
375
|
+
|
|
376
|
+
Warning: Note that in a dictionary, it will add quotes only to values,
|
|
377
|
+
not to keys.
|
|
378
|
+
|
|
379
|
+
>>> _add_quotes_to_str(222)
|
|
380
|
+
222
|
|
381
|
+
>>> _add_quotes_to_str("222")
|
|
382
|
+
"'222'"
|
|
383
|
+
>>> _add_quotes_to_str(("222", ))
|
|
384
|
+
'tuple of 1 item'
|
|
385
|
+
>>> _add_quotes_to_str(("222", 50, "String", 'Also string'))
|
|
386
|
+
'tuple of 4 items'
|
|
387
|
+
>>> _add_quotes_to_str(("222", 50, ["String", 'Also string']))
|
|
388
|
+
'tuple of 3 items'
|
|
389
|
+
>>> _add_quotes_to_str((["'whatever'", "'and whatever'"], 20))
|
|
390
|
+
'tuple of 2 items'
|
|
391
|
+
>>> _add_quotes_to_str({"a": "whatever", "b":"Something"})
|
|
392
|
+
{'a': "'whatever'", 'b': "'Something'"}
|
|
393
|
+
"""
|
|
394
|
+
if obj == "self":
|
|
395
|
+
return f"self"
|
|
396
|
+
if not obj:
|
|
397
|
+
return obj
|
|
398
|
+
|
|
399
|
+
if isinstance(obj, dict):
|
|
400
|
+
for k, v in obj.items():
|
|
401
|
+
obj[k] = _add_quotes_to_str(v)
|
|
402
|
+
return obj
|
|
403
|
+
if isinstance(obj, str):
|
|
404
|
+
return f"'{obj}'"
|
|
405
|
+
if isinstance(obj, range):
|
|
406
|
+
n = len(obj)
|
|
407
|
+
return f"range of {n} item{'s' if n > 1 else ''}"
|
|
408
|
+
if isinstance(obj, Iterable):
|
|
409
|
+
# For generator expression, we need to use itertools.tee,
|
|
410
|
+
# but this cannot be done behind the scenes, as the original
|
|
411
|
+
# object is empty and cannot be thus used.
|
|
412
|
+
try:
|
|
413
|
+
return type_and_len_of_iterable(obj)
|
|
414
|
+
except TypeError:
|
|
415
|
+
# cannot pickle the generator type,
|
|
416
|
+
# hence we do not get its length
|
|
417
|
+
return "generator"
|
|
418
|
+
|
|
419
|
+
return obj
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _stringify_args_kwargs(args, kwargs, digits=4):
|
|
423
|
+
"""Make a string out of args and kwargs.
|
|
424
|
+
|
|
425
|
+
The final string is formatted in a way that can be used in a print
|
|
426
|
+
of a call to a function. So, for instance, white spaces are added after
|
|
427
|
+
each argument; quotes are added to strings, etc.
|
|
428
|
+
|
|
429
|
+
>>> _stringify_args_kwargs(("something", 20), {})
|
|
430
|
+
"'something', 20"
|
|
431
|
+
>>> _stringify_args_kwargs(tuple(), {"x": 300})
|
|
432
|
+
'x=300'
|
|
433
|
+
>>> _stringify_args_kwargs(tuple(), {"x": "mixed"})
|
|
434
|
+
"x='mixed'"
|
|
435
|
+
>>> _stringify_args_kwargs(("something", 20), {"x": 300})
|
|
436
|
+
"'something', 20, x=300"
|
|
437
|
+
>>> _stringify_args_kwargs(("something", 20), {"x": "something else"})
|
|
438
|
+
"'something', 20, x='something else'"
|
|
439
|
+
"""
|
|
440
|
+
arguments = ""
|
|
441
|
+
if len(args) > 0 and len(kwargs) == 0:
|
|
442
|
+
if len(args) == 1:
|
|
443
|
+
a = _add_quotes_to_str(rounder.signif_object(args[0], digits))
|
|
444
|
+
arguments = f"{a}"
|
|
445
|
+
else:
|
|
446
|
+
to_join = [str(_add_quotes_to_str(rounder.signif_object(a, digits))) for a in args]
|
|
447
|
+
arguments = f"{', '.join(to_join)}"
|
|
448
|
+
elif len(kwargs) > 0 and len(args) == 0:
|
|
449
|
+
a = ", ".join(f"{k}={str(_add_quotes_to_str(rounder.signif_object(v, digits)))}" for k, v in kwargs.items())
|
|
450
|
+
arguments = f'{a}'
|
|
451
|
+
elif len(kwargs) > 0 and len(args) > 0:
|
|
452
|
+
if len(args) == 1:
|
|
453
|
+
arguments = f"{_add_quotes_to_str(rounder.signif_object(args[0], digits))}"
|
|
454
|
+
else:
|
|
455
|
+
to_join = [str(_add_quotes_to_str(rounder.signif_object(a, digits))) for a in args]
|
|
456
|
+
arguments = f"{', '.join(to_join)}"
|
|
457
|
+
if len(kwargs) == 1:
|
|
458
|
+
key = next(iter(kwargs))
|
|
459
|
+
arguments += f", {key}={_add_quotes_to_str(rounder.signif_object(kwargs[key], digits))}"
|
|
460
|
+
else:
|
|
461
|
+
arguments += f', {", ".join(f"{k}={_add_quotes_to_str(rounder.signif_object(v, digits))}" for k, v in kwargs.items())}'
|
|
462
|
+
return arguments
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# The final creation of the instance of Calls
|
|
466
|
+
|
|
467
|
+
calls = Calls()
|
|
468
|
+
calls.analyze = CallsAnalyzer()
|
|
469
|
+
calls.save = CallsSaver()
|
|
470
|
+
calls.read = CallsReader()
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
if __name__ == "__main__":
|
|
474
|
+
import doctest
|
|
475
|
+
|
|
476
|
+
doctest.testmod()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: perftester
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Lightweight performance testing in Python
|
|
5
5
|
Home-page: https://github.com/nyggus/perftester
|
|
6
6
|
Author: Nyggus
|
|
@@ -13,11 +13,11 @@ Classifier: Operating System :: OS Independent
|
|
|
13
13
|
Requires-Python: >=3.8
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
Requires-Dist: easycheck
|
|
16
|
-
Requires-Dist: memory-profiler
|
|
16
|
+
Requires-Dist: memory-profiler
|
|
17
17
|
Requires-Dist: rounder
|
|
18
18
|
Provides-Extra: dev
|
|
19
19
|
Requires-Dist: black ; extra == 'dev'
|
|
20
|
-
Requires-Dist: wheel
|
|
20
|
+
Requires-Dist: wheel ; extra == 'dev'
|
|
21
21
|
|
|
22
22
|
# `perftester`: Lightweight performance testing of Python functions
|
|
23
23
|
|
|
@@ -49,7 +49,7 @@ pt.time_benchmark(foo, x=129, n=100)
|
|
|
49
49
|
```
|
|
50
50
|
and this will print the results of the time benchmark, with raw results similar to those that `timeit.repeat()` returns, but unlike it, `pt.time_benchmark()` returns mean raw time per function run, not overall; in additional, you will see some summaries of the results.
|
|
51
51
|
|
|
52
|
-
The above call did actually run `timeit.repeat()` function, with the default configuration of `
|
|
52
|
+
The above call did actually run `timeit.repeat()` function, with the default configuration of `Number=100_000` and `Repeat=5`. If you want to change any of these, you can use arguments `Number` and `Repeat`, correspondigly:
|
|
53
53
|
|
|
54
54
|
```python
|
|
55
55
|
pt.time_benchmark(foo, x=129, n=100, Number=1000)
|
|
@@ -59,7 +59,7 @@ pt.time_benchmark(foo, x=129, n=100, Number=1000, Repeat=2)
|
|
|
59
59
|
|
|
60
60
|
These calls do not change the default settings so you use the arguments' values on the fly. Later you will learn how to change the default settings and the settings for a particular function.
|
|
61
61
|
|
|
62
|
-
> Some of you may wonder why the `Number` and `Repeat` arguments violate what we can call the Pythonic style, by using a capital first letter for function arguments. The reason is simple: I wanted to minimize a risk of conflicts that would happen when benchmarking (or testing) a function with any of the arguments `
|
|
62
|
+
> Some of you may wonder why the `Number` and `Repeat` arguments violate what we can call the Pythonic style, by using a capital first letter for function arguments. The reason is simple: I wanted to minimize a risk of conflicts that would happen when benchmarking (or testing) a function with any of the arguments `Number` or `Repeat` (or both). A chance that a Python function will have a `Number` or a `Repeat` argument is rather small. If that happens, however, you can use `functools.partial()` to overcome the problem:
|
|
63
63
|
|
|
64
64
|
```python
|
|
65
65
|
from functools import partial
|
|
@@ -217,7 +217,7 @@ To create a performance test for a function, you likely need to know how it beha
|
|
|
217
217
|
```python
|
|
218
218
|
>>> import perftester as pt
|
|
219
219
|
>>> def f(n): return sum(map(lambda i: i**0.5, range(n)))
|
|
220
|
-
>>> pt.config.set(f, "time",
|
|
220
|
+
>>> pt.config.set(f, "time", Number=1000)
|
|
221
221
|
>>> b_100_time = pt.time_benchmark(f, n=100)
|
|
222
222
|
>>> b_100_memory = pt.memory_usage_benchmark(f, n=100)
|
|
223
223
|
>>> b_1000_time = pt.time_benchmark(f, n=1000)
|
|
@@ -334,7 +334,7 @@ The whole configuration is stored in the `pt.config` object, which you can easil
|
|
|
334
334
|
|
|
335
335
|
```python
|
|
336
336
|
>>> def f(n): return list(range(n))
|
|
337
|
-
>>> pt.config.set(f, "time",
|
|
337
|
+
>>> pt.config.set(f, "time", Number=10_000, Repeat=1)
|
|
338
338
|
|
|
339
339
|
```
|
|
340
340
|
|
|
@@ -347,7 +347,7 @@ When you use `perftester` as a command-line tool, you can modify `pt.config` in
|
|
|
347
347
|
import perftester as pt
|
|
348
348
|
|
|
349
349
|
# shorten the tests
|
|
350
|
-
pt.config.set_defaults("time",
|
|
350
|
+
pt.config.set_defaults("time", Number=10_000, Repeat=3)
|
|
351
351
|
|
|
352
352
|
# log the results to file (they will be printed in the console anyway)
|
|
353
353
|
pt.config.log_to_file = True
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
perftester/__init__.py,sha256=x9O2UW8vfTuF7uKzPUeZ397FEzjcv42X2H26je9cyOI,273
|
|
2
|
+
perftester/__main__.py,sha256=aX_J60lLY2yoz5jXtNoqTnAE_wm_s4llQHeR3z2Mx68,5119
|
|
3
|
+
perftester/perftester.py,sha256=q8fj5hl0vW5rmB5FmdVqKZ5Vk43SkMgsC8tLX8m77Gw,31559
|
|
4
|
+
perftester/tmp.py,sha256=jqCDGCRO5fv7Uv7cJLu4PNmnaUMIApuu3nT_2zQwWqQ,1657
|
|
5
|
+
perftester/tmp_working.py,sha256=7ub5M6PFFfhSTCp_a-YQOZSFD05VNxa8Y7tDnEcBZzk,820
|
|
6
|
+
perftester/understand.py,sha256=H70Yjt3MPSB8rSjEi88Rwik15ZaVKn7OFBizC5H72NA,15670
|
|
7
|
+
perftester-0.5.1.dist-info/LICENSE,sha256=mZFAdfuYFAyBYiir4m3CTQu151mpXbMbh7Mm5M1bZAE,1063
|
|
8
|
+
perftester-0.5.1.dist-info/METADATA,sha256=gieG4jHDVbnwmfPwh_RB8jRlWnHPvjtiQ06SmfP1lgM,24670
|
|
9
|
+
perftester-0.5.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
10
|
+
perftester-0.5.1.dist-info/entry_points.txt,sha256=gM6Vf1BEeLY-1X9IQlO1TPAO3lJ5vToKfnJHT4MruIk,57
|
|
11
|
+
perftester-0.5.1.dist-info/top_level.txt,sha256=i1-4oWlkta2MsNKlZwJCibhn7aBexQfxncoPy2a6dfA,11
|
|
12
|
+
perftester-0.5.1.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
perftester/__init__.py,sha256=x9O2UW8vfTuF7uKzPUeZ397FEzjcv42X2H26je9cyOI,273
|
|
2
|
-
perftester/__main__.py,sha256=aX_J60lLY2yoz5jXtNoqTnAE_wm_s4llQHeR3z2Mx68,5119
|
|
3
|
-
perftester/perftester.py,sha256=gSM59rgPta5KXgsjzOcbEvPnAmBZhlvArnNsxR5SgBw,30965
|
|
4
|
-
perftester-0.4.0.dist-info/LICENSE,sha256=mZFAdfuYFAyBYiir4m3CTQu151mpXbMbh7Mm5M1bZAE,1063
|
|
5
|
-
perftester-0.4.0.dist-info/METADATA,sha256=v6roxxBxpqZNXC1SbuYrIUJVbR-_c0I2kctz2GqzRtE,24692
|
|
6
|
-
perftester-0.4.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
7
|
-
perftester-0.4.0.dist-info/entry_points.txt,sha256=gM6Vf1BEeLY-1X9IQlO1TPAO3lJ5vToKfnJHT4MruIk,57
|
|
8
|
-
perftester-0.4.0.dist-info/top_level.txt,sha256=i1-4oWlkta2MsNKlZwJCibhn7aBexQfxncoPy2a6dfA,11
|
|
9
|
-
perftester-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|