\[ [Index](index.md) | [Exercise 7.1](ex7_1.md) | [Exercise 7.3](ex7_3.md) \]

# Exercise 7.2

*Objectives:*

- Decorator chaining
- Defining decorators that accept arguments.

*Files Modified:* `logcall.py`, `validate.py`

## (a) Copying Metadata

When a function gets wrapped by a decorator, you often lose
information about the name of the function, documentation strings, and
other details.  Verify this:

```python
>>> @logged
    def add(x,y):
        'Adds two things'
        return x+y

>>> add
<function wrapper at 0x4439b0>
>>> help(add)
... look at the output ...
>>>
```

Fix the definition of the `logged` decorator so that it copies
function metadata properly.  To do this, use the `@wraps(func)`
decorator as shown in the notes.

After you're done, make sure the decorator preserves the function name
and doc string.

```python
>>> @logged
    def add(x,y):
        'Adds two things'
        return x+y

>>> add
<function add at 0x4439b0>
>>> add.__doc__
'Adds two things'
>>>
```

Fix the `@validated` decorator you wrote earlier so that it also preserves
metadata using `@wraps(func)`.

## (b) Your first decorator with arguments

The `@logged` decorator you defined earlier always just
prints a simple message with the function name.
Suppose that you wanted the user to be able to specify a 
custom message of some sort.

Define a new decorator `@logformat(fmt)` that accepts
a format string as an argument and uses `fmt.format(func=func)` to
format a supplied function into a log message:

```python
# sample.py
...
from logcall import logformat

@logformat('{func.__code__.co_filename}:{func.__name__}')
def mul(x,y):
    return x*y
```

To do this, you need to define a decorator that takes an argument.
This is what it should look like when you test it:

```python
>>> import sample
Adding logging to add
Adding logging to sub
Adding logging to mul
>>> sample.add(2,3)
Calling add
5
>>> sample.mul(2,3)
sample.py:mul
6
>>>
```

To further simplify the code, show how you can define the original `@logged` decorator
using the the `@logformat` decorator. 

## (c) Multiple decorators and methods

Things can get a bit dicey when decorators are applied to methods in a
class.  Try applying your `@logged` decorator to the methods in the
following class.

```python
class Spam:
    @logged
    def instance_method(self):
        pass

    @logged
    @classmethod
    def class_method(cls):
        pass

    @logged
    @staticmethod
    def static_method():
        pass

    @logged
    @property
    def property_method(self):
        pass
```

Does it even work at all? (hint: no). Is there any way to fix the code so
that it works?  For example, can you make it so the following example
works?

```python
>>> s = Spam()
>>> s.instance_method()
instance_method
>>> Spam.class_method()
class_method
>>> Spam.static_method()
static_method
>>> s.property_method
property_method
>>>
```

## (d) Validation (Redux)

In the last exercise, you wrote a `@validated` decorator that enforced
type annotations. For example:

```python
@validated
def add(x: Integer, y:Integer) -> Integer:
    return x + y
```

Make a new decorator `@enforce()` that enforces types specified
via keyword arguments to the decorator instead.  For example:

```python
@enforce(x=Integer, y=Integer, return_=Integer)
def add(x, y):
    return x + y
```

The resulting behavior of the decorated function should be identical.
Note: Make the `return_` keyword specify the return type.  `return` is
a Python reserved word so you have to pick a slightly different name.


**Discussion**

Writing robust decorators is often a lot harder than it looks.
Recommended reading:


\[ [Solution](soln7_2.md) | [Index](index.md) | [Exercise 7.1](ex7_1.md) | [Exercise 7.3](ex7_3.md) \]

----
`>>>` Advanced Python Mastery  
`...` A course by [dabeaz](https://www.dabeaz.com)  
`...` Copyright 2007-2023  

![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
