0

Let's say we have multiple functions which all accept an URL as their first argument and this URL needs to be validated. This can be nicely solved with a decorator

def validate_url(f):
   def validated(url, *args, **kwargs):
       assert len(url.split('.')) == 3 # trivial example
       return f(url, *args, **kwargs)
    return validated

@validate_url
def some_func(url, some_other_arg, *some_args, **some_kwargs):
    pass

This approach will work and allow me to factor the validation behavior out of many instances of similar functions. But now I would want to write a class method which also takes a validated URL. However, the naive approach will not work

class SomeClass:
    @validate_url
    def some_method(self, url, some_other_args):
        pass

because we will end up attempting to validate self and not url. My question is how to write a single decorator which will work for both functions and methods with the minimum amount of boilerplate.

Note 1: I am aware why this happens, it's just that I don't know how to work around this in the most elegant manner.

Note 2: The URL validation problem is just an example, so checking if isinstance(args[0], str) is not a good solution.

Jatentaki
  • 8,339
  • 2
  • 33
  • 32
  • Not sure whether [this](https://stackoverflow.com/a/53322/6699447) can be applied here – Abdul Niyas P M Sep 18 '18 at 17:28
  • Could you define a `@staticmethod` like `def validate_url(url, *args)` that runs inside of `some_func`? You could then decorate `validate_url`. Seems hacky and non-Pythonic, but that's the first thing I'd try – C.Nivs Sep 18 '18 at 19:27

1 Answers1

1

One solution would be to somehow detect whether the decorated function is a class method or not — which seems to be difficult if not impossible (as far as I can tell anyway) to do so cleanly. The inspect module's ismethod() and isfunction() don't work inside a decorator used inside a class definition.

Given that, here's a somewhat hacky way of doing it which checks to see if the decorated callable's first argument has been given the name "self", which is the coding convention for it in class methods (although it is not a requirement, so caveat emptor and use at your own risk).

The following code seems to work in both Python 2 and 3. However in Python 3 it may raise DeprecationWarnings depending on exactly what sub-version is being used—so they have been suppressed in a section of the code below.

from functools import wraps
import inspect
import warnings

def validate_url(f):
    @wraps(f)
    def validated(*args, **kwargs):
        with warnings.catch_warnings():
            # Suppress DeprecationWarnings in this section.
            warnings.simplefilter('ignore', category=DeprecationWarning)

            # If "f"'s first argument is named "self",
            # assume it's a method.
            if inspect.getargspec(f).args[0] == 'self':
                url = args[1]
            else:  # Otherwise assume "f" is a ordinary function.
                url = args[0]
        print('testing url: {!r}'.format(url))
        assert len(url.split('.')) == 3  # Trivial "validation".
        return f(*args, **kwargs)
    return validated

@validate_url
def some_func(url, some_other_arg, *some_args, **some_kwargs):
    print('some_func() called')


class SomeClass:
    @validate_url
    def some_method(self, url, some_other_args):
        print('some_method() called')


if __name__ == '__main__':
    print('** Testing decorated function **')
    some_func('xxx.yyy.zzz', 'another arg')
    print('  URL OK')
    try:
        some_func('https://bogus_url.com', 'another thing')
    except AssertionError:
        print('  INVALID URL!')

    print('\n** Testing decorated method **')
    instance = SomeClass()
    instance.some_method('aaa.bbb.ccc', 'something else')  # -> AssertionError
    print('  URL OK')
    try:
        instance.some_method('foo.bar', 'arg 2')  # -> AssertionError
    except AssertionError:
        print('  INVALID URL!')

Output:

** Testing decorated function **
testing url: 'xxx.yyy.zzz'
some_func() called
  URL OK
testing url: 'https://bogus_url.com'
  INVALID URL!

** Testing decorated method **
testing url: 'aaa.bbb.ccc'
some_method() called
  URL OK
testing url: 'foo.bar'
  INVALID URL!
martineau
  • 99,260
  • 22
  • 139
  • 249