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 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 from d_settings. Allow but ignore attempts to write to immutable keys (max_history). This permits the user to retrieve a copy of the settings with as_dict() or as_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 to update().

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