Call History and Statistics¶
Unless it’s bypassed, log_calls always collects at least
a few basic statistics about each call to a decorated callable.
It can collect the entire history of calls to a function or method if asked
to (using the record_history setting).
The statistics and history are accessible via the stats
attribute
that log_calls adds to the wrapper of a decorated callable.
The two settings parameters we haven’t yet discussed govern the recording and retention of a decorated callable’s call history.
The record_history
parameter (default: False
)¶
When the record_history
setting is true for a decorated callable,
log_calls accumulates a sequence of records holding the details of each
logged call to the callable:
A logged call to a decorated callable is one that occurs when the callable’senabled
setting is true.
That history is accessible via attributes of the stats
object.
Let’s define a function f
with record_history
set to true:
>>> @log_calls(record_history=True, log_call_numbers=True, log_exit=False)
... def f(a, *args, x=1, **kwargs): pass
In the next few sections, we’ll call this function, manipulate its settings, and examine its statistics and history.
The max_history
parameter (default: 0
)¶
The max_history
parameter bounds the number of call history records retained
in a decorated callable’s recorded call history. If this value is 0 or negative,
unboundedly many records are retained (unless or until you change the record_history
setting to false, or call the stats.clear_history()
method). If the value of
max_history
is > 0, call history operates as a least-recently-used cache:
log_calls will retain at most that many records, discarding the oldest record
to make room for a new ones if the history is at capacity.
You cannot change max_history
using the mapping interface or the attribute
of the same name; attempts to do so raise ValueError
. The only way to change
its value is with the stats.clear_history(max_history=0)
method, discussed
below.
The stats
attribute and its attributes¶
The stats
attribute of a decorated callable is an object that provides
read-only statistics and data about the calls to a decorated callable:
- stats.num_calls_logged
- stats.num_calls_total
- stats.elapsed_secs_logged
- stats.process_secs_logged
- stats.history
- stats.history_as_csv
- stats.history_as_DataFrame
The first four of these don’t depend on the record_history
setting at all.
The last three values, stats.history*
, are empty unless record_history
is or has been true.
The stats
attribute also provides one method, stats.clear_history().
Let’s call the above-defined function f
twice:
>>> f(0)
f [1] <== called by <module>
arguments: a=0
defaults: x=1
>>> f(1, 100, 101, x=1000, y=1001)
f [2] <== called by <module>
arguments: a=1, *args=(100, 101), x=1000, **kwargs={'y': 1001}
and explore its stats
.
The stats.num_calls_logged
attribute¶
The stats.num_calls_logged
attribute holds the number of the most
recent logged call to a decorated callable. Thus, f.stats.num_calls_logged
will equal 2:
>>> f.stats.num_calls_logged
2
This counter is incremented on each logged call to the callable, even if its
log_call_numbers
setting is false.
The stats.num_calls_total
attribute¶
The stats.num_calls_total
attribute holds the total number of calls
to a decorated callable. This counter is incremented even when logging
is disabled for a callable (its enabled
setting is False
, i.e. 0
),
but not when it’s bypassed.
To illustrate, let’s now disable logging for f
and call it 3 more times:
>>> f.log_calls_settings.enabled = False
>>> for i in range(3): f(i)
Now f.stats.num_calls_total
will equal 5, but f.stats.num_calls_logged
will still equal 2:
>>> f.stats.num_calls_total
5
>>> f.stats.num_calls_logged
2
Finally, let’s re-enable logging for f
and call it again.
The displayed call number will be the number of the logged call, 3, the same
value as f.stats.num_calls_logged
after the call:
>>> f.log_calls_settings.enabled = True
>>> f(10, 20, z=5000)
f [3] <== called by <module>
arguments: a=10, *args=(20,), **kwargs={'z': 5000}
defaults: x=1
>>> f.stats.num_calls_total
6
>>> f.stats.num_calls_logged
3
The stats.elapsed_secs_logged
attribute¶
The stats.elapsed_secs_logged
attribute holds the sum of the elapsed times of
all logged calls to a decorated callable, in seconds. Here’s its value for the three
logged calls to f
above (this doctest is actually +SKIP
ped):
>>> f.stats.elapsed_secs_logged
6.67572021484375e-06
The stats.process_secs_logged
attribute¶
The stats.process_secs_logged
attribute holds the sum of the process times
of all logged calls to a decorated callable, in seconds.
Here’s its value for the three logged calls to f
above (this doctest is
actually +SKIP
ped):
>>> f.stats.process_secs_logged
1.1000000000038757e-05
The stats.history
attribute¶
The stats.history
attribute of a decorated callable provides the call history
of logged calls as a tuple of records. Each record is a namedtuple
of type CallRecord
, whose fields are those shown in the following example.
Here’s f
‘s call history, output reformatted for readability:
>>> print('\\n'.join(map(str, f.stats.history)))
CallRecord(call_num=1, argnames=['a'], argvals=(0,), varargs=(),
explicit_kwargs=OrderedDict(),
defaulted_kwargs=OrderedDict([('x', 1)]), implicit_kwargs={},
retval=None,
elapsed_secs=3.0049995984882116e-06,
process_secs=2.9999999999752447e-06,
timestamp='10/28/14 15:56:13.733763',
prefixed_func_name='f', caller_chain=['<module>'])
CallRecord(call_num=2, argnames=['a'], argvals=(1,), varargs=(100, 101),
explicit_kwargs=OrderedDict([('x', 1000)]),
defaulted_kwargs=OrderedDict(), implicit_kwargs={'y': 1001},
retval=None,
elapsed_secs=3.274002665420994e-06,
process_secs=3.0000000000030003e-06,
timestamp='10/28/14 15:56:13.734102',
prefixed_func_name='f', caller_chain=['<module>'])
CallRecord(call_num=3, argnames=['a'], argvals=(10,), varargs=(20,),
explicit_kwargs=OrderedDict(),
defaulted_kwargs=OrderedDict([('x', 1)]), implicit_kwargs={'z': 5000},
retval=None,
elapsed_secs=2.8769973141606897e-06,
process_secs=2.9999999999752447e-06,
timestamp='10/28/14 15:56:13.734412',
prefixed_func_name='f', caller_chain=['<module>'])
The CSV representation, discussed next, pairs the argnames
with their values
in argvals
(each parameter name in argnames
become a column heading),
making it more human-readable, especially when viewed in a program that presents
CSVs nicely.
The stats.history_as_csv
attribute¶
The value stats.history_as_csv
attribute is a text representation in CSV format
of a decorated callable’s call history. You can save this string
and import it into the program or tool of your choice for further analysis.
(If your tool of choice is Pandas, you can use
The stats.history_as_DataFrame attribute, discussed next, to obtain history directly in
the representation you really want.)
The CSV representation breaks out each argument into its own column, throwing away information about whether an argument’s value was explicitly passed or is a default.
The CSV separator is '|'
rather than ','
because some of the fields – args
, kwargs
and caller_chain
– use commas intrinsically. Let’s examine history_as_csv
for a function that has all of those fields nontrivially:
>>> @log_calls(record_history=True, log_call_numbers=True,
... log_exit=False, log_args=False)
... def f(a, *extra_args, x=1, **kw_args): pass
>>> def g(a, *args, **kwargs):
... f(a, *args, **kwargs)
>>> @log_calls(log_exit=False, log_args=False)
... def h(a, *args, **kwargs):
... g(a, *args, **kwargs)
>>> h(0)
h <== called by <module>
f [1] <== called by g <== h
>>> h(10, 17, 19, z=100)
h <== called by <module>
f [2] <== called by g <== h
>>> h(20, 3, 4, 6, x=5, y='Yarborough', z=100)
h <== called by <module>
f [3] <== called by g <== h
Here’s the call history of f
in CSV format (ellipses added for the elapsed_secs
,
process_secs
and timestamp
fields):
>>> print(f.stats.history_as_csv)
call_num|a|extra_args|x|kw_args|retval|elapsed_secs|process_secs|timestamp|prefixed_fname|caller_chain
1|0|()|1|{}|None|...|...|...|'f'|['g', 'h']
2|10|(17, 19)|1|{'z': 100}|None|...|...|...|'f'|['g', 'h']
3|20|(3, 4, 6)|5|{'y': 'Yarborough', 'z': 100}|None|...|...|...|'f'|['g', 'h']
In tabular form,
call_num | a | extra_args | x | kw_args | retval | elapsed_secs | process_secs | timestamp | prefixed_fname | caller_chain |
---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | () | 1 | {}
|
None | ... | ... | ... | ‘f’ | [‘g’, ‘h’] |
2 | 10 | (17, 19) | 1 | {‘z’: 100}
|
None | ... | ... | ... | ‘f’ | [‘g’, ‘h’] |
3 | 20 | (3, 4, 6) | 5 | {‘y’: ‘Yarborough’,
‘z’: 100}
|
None | ... | ... | ... | ‘f’ | [‘g’, ‘h’] |
As usual, log_calls will use whatever names you use for varargs parameters
(here, extra_args
and kw_args
). Whatever the name of the kwargs
parameter,
items within that field are guaranteed to be in sorted order.
The stats.history_as_DataFrame
attribute¶
The stats.history_as_DataFrame
attribute returns the history of a decorated
callable as a Pandas
DataFrame,
if the Pandas library is installed. This saves you the intermediate step of
calling DataFrame.from_csv
with the proper arguments (and also saves you from
having to know or care what those are).
If Pandas is not installed, the value of this attribute is None
.
The stats.clear_history(max_history=0)
method¶
As you might expect, the stats.clear_history(max_history=0)
method clears
the call history of a decorated callable. In addition, it resets all running sums:
num_calls_total
andnum_calls_logged
are reset to0
,elapsed_secs_logged
andprocess_secs_logged
are reset to0.0
.
This method is the only way to change the value of the ``max_history`` setting,
via the optional keyword parameter for which you can supply any (integer) value,
by default 0
.
The function f
has a nonempty history, as we just saw. Let’s clear f
‘s history,
setting max_history
to 33
:
>>> f.stats.clear_history(max_history=33)
and check that settings and `stats`
tallies are reset:
>>> f.log_calls_settings.max_history
33
>>> f.stats.num_calls_logged
0
>>> f.stats.num_calls_total
0
>>> f.stats.elapsed_secs_logged
0.0
>>> f.stats.process_secs_logged
0.0