Python decorator class with members

254 Views Asked by At

I want to write a class that will have member variables and member functions that will be used as decorators.

class decorator_class:
   def __init__(self, baseurl, username,password):
     self._baseurl = baseurl
     self._username = _username
     self._password = _password

   def decorator_get(self,arguments):
        def inner_function(function):
            @wraps(function)
            def wrapper(*args, **kwargs):
                url = self._url + argument
                if len(kwargs) > 0:
                    url+="?"
                    argseperator=""
                    for k,v in kwargs.items():
                        url+="{}{}={}".format(argseperator,k,v)
                        argseperator="&"
                    r = requests.get(url, auth=(self._username, self._password))
                    if r.status_code != 200:
                        raise Exception('Failed to GET URL: {}'.format(url))
                return function(args[0],json = r.json())
            return wrapper
        return inner_function

class usedecorator:
   def __init__(baseurl, self,user,password):
      self.dec = decorator_class(baseurl, self,user,password)
   
   @dec.decorator_get('/path/to/resource1')
   def process_output_resource1(self, json):
     do_something_with_json

The problem is that __init__ is being called after the class is loaded and at that time dec is undefined.

if I define the decorator_class globally it works, but then there is no way to pass the url, user and password to it at runtime.

Any suggestions?

3

There are 3 best solutions below

0
On

Your decorator_get > innder_function > wrapper has the userdecorator's self. Weird sentence but eh.

You have some weird namings, IDK why did you use self as a second argument for instance but, I tried to follow your naming.

def decorator_get(arguments):
     def inner_function(function):
         @wraps(function)
         def wrapper(self, *args, **kwargs):
             url = self._base_url + arguments
             if len(kwargs) > 0:
                 url+="?"
                 argseperator=""
                 for k,v in kwargs.items():
                     url+="{}{}={}".format(argseperator,k,v)
                     argseperator="&"
                 r = requests.get(url, auth=(self._username, self._password))
                 if r.status_code != 200:
                     raise Exception('Failed to GET URL: {}'.format(url))
             return function(self, json = r.json())
         return wrapper
     return inner_function


class usedecorator:
    def __init__(self, baseurl,user,password):
        self._base_url = baseurl
        self._username = user
        self._password= password
   
    @decorator_get('/path/to/resource1')
    def process_output_resource1(self, json):
        do_something_with_json
0
On

I think you're going too far with the decorator approach. Let's break this down into a single question: What is the actual shared state here that you need a class for? To me, it looks like just the baseurl, user, and password. So let's just use those directly without a decorator:

from requests import Session
from requests.auth import HTTPBasicAuth


class UseDecorator: # this isn't a good name, but we will keep it temporarily
    def __init__(self, baseurl, user, password):
        self.baseurl = baseurl
        self.session = Session()
        # we've now bound the authentication to the session
        self.session.auth = HTTPBasicAuth(user, password)


    # now let's just bind a uri argument to a function to simply
    # send a request
    def send_request(self, uri, *args, **kwargs):

        url = self.baseurl + uri

        # you don't need to manually inject parameters, just use
        # the params kwarg
        r = self.session.get(url, params=kwargs)

        # this will check the response code for you and even handle
        # a redirect, which your 200 check will fail on
        r.raise_for_status()

        return r.json()


    # then just handle each individual path
    def path_1(self, *args, **kwargs):
        data = self.send_request('/path/1', *args, **kwargs)
        # process data

    def path_2(self, *args, **kwargs):
        data = self.send_request('/path/2', *args, **kwargs)
        # process data

Because we're leveraging the machinery offered to us by requests, most of your decorator is simplified, and we can boil it down to a simple function call for each path

2
On

Indeed -to have a decorator for methods in a class, it must already be defined (i.e. ready to be used) when the method to be decorated is declared: which means it have to be declared either at top-level or inside the class body.

Code inside methods, including __init__, however will only run when an instance is created - and that is the point where the class will get your connection parameters.

If this decorator is being used always in this model, you can turn it into a descriptor: an object which is a class attribute, but which has code (in a method named __get__) that is executed after the instance is created.

This descriptor could then fetch the connection parameters in the instance itself, after it has been created, and prepare way for calling the underlying method.

That will require some reorganization on your code: the object returned by __get__ has to be a callable which will ultimately run your function, but it would not be nice if simply retrieving the method name would trigger the network request - one will expect it to be triggered when the process_output... method is actually called. The __get__ method then should return your inner "wrapper" function, which will have all the needed data for the request from the "instance" attribute Python passes automatically, but for the payload which it gets via kwargs.

class decorator_class:
    def __init__(self, path=None):
        self.path = None
        self.func = None


    def __get__(self, instance, owner):
        if instance is None:
            return self
        def bound_to_request(**kwargs):
            # retrieves the baseurl, user and password from the host instance:
            # build query part of the target URL - instead of your  convoluted
            # code to build the query string (which will break ont he first special character,
            # just pass kwargs as the "params" argument)
            response = requests.get(instance._base_url, auth=(
                instance.user, instance.password), params=kwargs)
            # error treatment code
            #...
            return self.func(response.json()) 
        return bound_to_request
    
    
    def __call__(self, arg):
        # create a new instance of this class on each stage:
        # first anotate the API path, on the second call annotate the actual method
   
        new_inst = type(self)()
        if not self.path:
            if not isinstance(arg, str):
                raise TypeError("Expecting an API path for this method")
            new_inst.path = arg
        else:
            if not callable(arg):
                raise TypeError("Expecting a target method to be decorated")
            new_inst.func = wraps(arg)
        return new_inst

    def __repr__(self):
        return f"{self.func.__name__!r} method bound to retrieve data from {self.path!r}"

class use_decorator:
    dec = decorator_class()
    def __init__(self=, baseurl, user, password):
        # the decorator assumes these to be set as instance attributes
        self.baseurl = baseurl
        self.user = user
        self.password = password
      
    # <- the call passing the path returns an instance of
    #  the decorator with the path set. it is use as an
    #  decorator is called again, and on this second call, the decorated method is set.
   @dec.decorator_get('/path/to/resource1') 
   def process_output_resource1(self, json):
       # do_something_with_json
       ...

In time, re-reading your opening paragraph, I see you intended to have more than one decorator inside your original class, probably others intended for "POST" and other HTTP requests: most important thing, the __get__ name here has nothing to do with HTTP: it is a fixed method name in the Python spec which is called automatically by the language when one will retrieve your method from an instance of use_decorator. That is, when there is code: my_instance.process_output_resource1(...), the __get__ method of the descriptor is called. Whatever it returned is then called.

For enabling the same decorator to use POST and other HTTP methods, I suggest you to have as a first parameter when annotating the path for each method, and then simply call the appropriate requests method by checking self.method inside the bound_to_request function.