Dynamic Control of Settings¶
Sometimes, you’ll need or want to change a log_calls setting for a decorated callable on the fly. The major impediment to doing so is that the values of the log_calls parameters are set once the definition of the decorated callable is interpreted. Those values are established once and for all when the Python interpreter processes the definition.
The problem, and log_calls solutions¶
Even if a variable is used as a parameter value, its value at the time Python processes the definition is “frozen” for the created callable object. What gets stored is not the variable, but its value. Subsequently changing the value of the variable will not affect the behavior of the decorator.
For example, suppose DEBUG
is a module-level variable initialized to False
:
>>> DEBUG = False
and you use this code:
>>> @log_calls(enabled=DEBUG)
... def foo(**kwargs): pass
>>> foo() # No log_calls output: DEBUG is False
If later you set DEBUG = True
and call foo
, that call still won’t be logged,
because the enabled
setting of foo
is bound to the original value
of DEBUG
, established when the definition was processed:
>>> DEBUG = True
>>> foo() # Still no log_calls output
This is simply how default values of keyword parameters work in Python.
log_calls provides three ways to overcome this limitation and dynamically control the settings of a decorated callable:
- the
decorate_*
classmethods, described in the previous chapter Bulk (Re)Decoration, (Re)Decorating Imports, - the
log_calls_settings
attribute, described in this chapter, which provides a mapping interface and an attribute-based interface to settings, and - indirect values, as described in the next chapter Indirect Values of Keyword Parameters.
The log_calls_settings
attribute — the settings API¶
log_calls adds an attribute log_calls_settings
to the wrapper of a decorated callable,
through which you can access the settings for that callable. This attribute
is an object that lets you read and write the settings of the callable via a mapping
(dict
-like) interface, and equivalently, via attributes of the object. The mapping keys
and the attribute names are simply the log_calls settings keywords. log_calls_settings
also
implements many of the standard dict
methods for interacting with the settings in familiar ways.
The mapping interface and the attribute interface to settings¶
Once you’ve decorated a callable with log_calls,
>>> @log_calls()
... def f(*args, **kwargs):
... return 91
you can access and change its settings via the log_calls_settings
attribute
of the decorated callable, which behaves like a dictionary. You can read and
write settings using the log_calls keywords as keys:
>>> f.log_calls_settings['enabled']
True
>>> f.log_calls_settings['enabled'] = False
>>> _ = f() # no output (not even 91, because of "_ = ")
>>> f.log_calls_settings['enabled']
False
>>> f.log_calls_settings['log_retval']
False
>>> f.log_calls_settings['log_retval'] = True
>>> f.log_calls_settings['log_elapsed']
False
>>> f.log_calls_settings['log_elapsed'] = True
You can also use the same keywords as attributes of log_calls_settings
instead of as keys to the mapping interface — they’re completely equivalent:
>>> f.log_calls_settings.log_elapsed
True
>>> f.log_calls_settings.log_call_numbers
False
>>> f.log_calls_settings.log_call_numbers = True
>>> f.log_calls_settings.enabled = True # turn it back on!
>>> _ = f()
f [1] <== called by <module>
arguments: <none>
f [1] return value: 91
elapsed time: ... [secs], process time: ... [secs]
f [1] ==> returning to <module>
>>> f.log_calls_settings.log_args = False
>>> f.log_calls_settings.log_elapsed = False
>>> f.log_calls_settings.log_retval = False
>>> _ = f()
f [2] <== called by <module>
f [2] ==> returning to <module>
log_calls_settings
has a length len(log_calls_settings)
;
its keys and items()
can be iterated through; you can use in
to test
for key membership; and it has an update()
method. As with an ordinary dictionary,
attempting to access a nonexistent setting raises KeyError
. Unlike an ordinary
dictionary, you can’t add new keys – the log_calls_settings
dictionary is closed
to new members, and attempts to add one will also raise KeyError
.
The update()
, as_dict()
, and as_OD()
methods¶
The update()
method of the log_calls_settings
object lets you update several settings at once:
>>> f.log_calls_settings.update(
... log_args=True, log_elapsed=False, log_call_numbers=False,
... log_retval=False)
>>> _ = f()
f <== called by <module>
arguments: <none>
f ==> returning to <module>
You can retrieve the entire collection of settings as a dict
using as_dict()
,
and as an OrderedDict
using as_OD()
. Either can serve as a snapshot
of the settings, so that you can change settings temporarily, use the new settings,
and then use update()
to restore settings from the snapshot. in addition to taking
keyword arguments, as shown above, update()
can take one or more dicts – in
particular, a dictionary retrieved from one of the as_*
methods:
-
wrapper.log_calls_settings.
update
(*dicts, **d_settings) → None Update the settings from all dicts in
dicts
, in order, and then fromd_settings
. Allow but ignore attempts to write to immutable keys (max_history
). This permits the user to retrieve a copy of the settings withas_dict()
oras_OD()
, obtaining a dictionary which will contain items for immutable settings too; make changes to settings and use them; then restore the original settings by passing the retrieved dictionary toupdate()
.Parameters: - dicts – a sequence of dicts containing setting keywords and values
- d_settings – additional settings and values
Example¶
This example illustrates the use-case described above.
First, retrieve settings (here, as an OrderedDict
because those are
more doctest-friendly, but in “real life” using as_dict()
suffices):
>>> od = f.log_calls_settings.as_OD()
>>> od
OrderedDict([('enabled', True), ('args_sep', ', '),
('log_args', True), ('log_retval', False),
('log_elapsed', False), ('log_exit', True),
('indent', True), ('log_call_numbers', False),
('prefix', ''), ('file', None),
('logger', None), ('loglevel', 10),
('mute', False),
('record_history', False), ('max_history', 0)])
Change settings temporarily:
>>> f.log_calls_settings.update(
... log_args=False, log_elapsed=True, log_call_numbers=True,
... log_retval=True)
Use the new settings for f
:
>>> _ = f()
f [4] <== called by <module>
f [4] return value: 91
elapsed time: ... [secs], process time: ... [secs]
f [4] ==> returning to <module>
Now restore original settings, this time passing the retrieved settings
dictionary rather than keywords (we could pass **od
, but that’s
unnecessary and a pointless expense):
>>> f.log_calls_settings.update(od)
>>> od == f.log_calls_settings.as_OD()
True