4

I was looking for a way to add some sort of decorator that applies to all instances of requests.get being used in any function.

For example,

@my_custom_decorator
def hello():
    ...
    r = requests.get('https://my-api-url')
    ...

The my_custom_decorator could then add a common param (or anything else) for all instances of requests.get. One will only need to add the decorator whereever requests.get is being used.

For now, I'm thinking of somehow checking if the original function contains presence of requests.get, but that seems not to be ideal.

Note: Also, I'm looking not to change any existing instances of requests.get..hence looking for a better way to achieve this.

xan
  • 4,640
  • 13
  • 50
  • 83
  • 7
    I don't think a decorator is the best way to solve this, maybe you should write your own `get` function instead, which calls the `requests.get` and adds whatever behaviour you want? – Florent Monin Mar 02 '23 at 15:54
  • @FlorentMonin is it possible if you can please point me or share few links/resources which can be helpful and I can take inspiration from? – xan Mar 02 '23 at 15:56
  • @FlorentMonin just to add: I'm looking not to change any instances of `requests.get`..hence a decorator came to my mind. – xan Mar 02 '23 at 16:02
  • 1
    Not sure what you mean by "not change any instances of `requests.get`". I posted an answer with more details on how to create a function that would implement the behaviour change you want – Florent Monin Mar 02 '23 at 16:05
  • @xan, *my_custom_decorator could then add a common param (or anything else)* - can you post some real-case common params which should be applied by such custom decorator? – RomanPerekhrest Mar 08 '23 at 18:12
  • @RomanPerekhrest, let's say my application has 100+ servers and want to pass a "server_id" (as param or even a header) for all requests from the application layer without touching/modifying a lot of existing code. – xan Mar 08 '23 at 18:28
  • @xan, do you have a lot of `requests.get` calls which you want to leave unchanged or can you switch to `requests.Session` ? – RomanPerekhrest Mar 08 '23 at 18:46
  • @RomanPerekhrest, if there's a way with using `requests.Session` - that would help too to some extent! We have a mixture of both. – xan Mar 08 '23 at 19:51
  • What behavior do you expect if `request.get` is called in a function `foo` called by `hello`? – Jorge Luis Mar 10 '23 at 11:02

8 Answers8

2

You can use unittest.mock.patch as a decorator on functions where requests.get needs additional parameters:

import requests
from unittest.mock import patch

def get(url, params=None, orig_get=requests.get, **kwargs):
    return orig_get(url, {**(params or {}), 'extra_param': 'extra_value'}, **kwargs)

my_custom_decorator = patch('requests.get', get)

so that:

@my_custom_decorator
def hello():
    r = requests.get('https://my-api-url', {'foo': 'bar'})
    print(r.status_code)

hello()

would send a GET request to the web server at my-api-url with the following URI:

/?foo=bar&extra_param=extra_value
blhsing
  • 91,368
  • 6
  • 71
  • 106
1

It would probably be very painful to use a decorator to solve this, since you would need to parse the code in the function (see this question for instance).

Creating your own get function would probably be more suited for your case (though it depends on what you actually want to do)

something like:

def get_with_param(url: str):
    # for instance, add a query param:
    r = requests.get(url + "?param=something")
    return r

and then you just have to replace every occurence of requests.get with get_with_param, for instance

Florent Monin
  • 1,278
  • 1
  • 3
  • 16
  • actually, I wanted to avoid this way as then I will have to replace every occurence of `requests.get`. I was trying to figure out any other way of avoiding this. – xan Mar 02 '23 at 16:10
  • 1
    @xan But now you will still need to wrap every function using `requests.get` with your decorator, so it sounds like the same amount of work. Also, it will be much easier to understand and debug this solution, as it uses a clear function rather than changing params to an existing function "behind the scenes". – Barak Fatal Mar 08 '23 at 15:42
  • @BarakFatal not the same amount since adding a decorator is much more easy and eliminates a lot of testing compared to actual code changes. Apart from a decorator, do you see any other workaround? maybe a wrapper? – xan Mar 10 '23 at 08:17
  • @xan so my personal opinion is that a "workaround" might be a bad idea. For the very least it will be a non-trivial solution, which means anyone else working on your code base would have to know about it. That's why I agree with this proposed answer, as it doesn't involve any workaround, even if it means more testing. – Barak Fatal Mar 11 '23 at 14:11
0

Not ideal/recommended depending on the context, but in the abscence of one, you are best placed to decide this. You could also try patch to monkeypatch requests.get.

from unittest.mock import patch
from requests import get as my_requests
import requests

def my_get(*args, **kwargs):
    ''' Your version of get where you do what needs to be done'''
    return my_requests(*args, **kwargs)

with patch('requests.get', my_get): # where you provide an alternate implementation for requests.get
    requests.get('https://www.google.com')

Anwar Husain
  • 1,414
  • 14
  • 19
0

In fact, you can't change "the instance" of requests.get because it returns

:return: :class:`Response <Response>` object

So, I recommend the next solution (requests's params was used for show case):

def my_custom_decorator(func):
    def wrapper(*args, **kwargs):
        params={"search":"string"}
        return func(*args, params=params, **kwargs)
    return wrapper

And, yeah, there is no way to avoid **kwargs:

@my_custom_decorator
def hello(**kwargs):
    resp = requests.get('https://mail.ru', **kwargs)

Take a look PoWs (before and after): enter image description here enter image description here

storenth
  • 967
  • 11
  • 18
  • @xan btw, you can remove `params` entries from decorator, and use my solution this way: `hello(params={"search":"string"})` to achieve same result! – storenth Mar 10 '23 at 11:53
0

I'm looking not to change any existing instances of requests.get

Is there a specific reason for that? Temporarily overriding requests.get seems to be a feasible solution.

#!/usr/bin/env python3
import requests

def deco(f):
  def my_fun(self):
    return "requests.get was overridden!"

  def inner():
    backup_fun = requests.get 
    requests.get = my_fun
    ret = f()
    requests.get = backup_fun
    return ret

  return inner

@deco
def decorated():
  return requests.get("http://example.org")

def undecorated():
  return requests.get("http://example.org")

if __name__ == '__main__':
  print("decorated:", decorated())
  print("undecorated:", undecorated())

Will print:

decorated: requests.get was overridden!
undecorated: <Response [200]>
etuardu
  • 5,066
  • 3
  • 46
  • 58
0

We can modify and/or add other parameters like here where I modify the url parameter for some_requests_dot_get_function via some_decorator:

url = 'google.com'#'https://www.google.com/'
def some_decorator(some_function):
    def wrapper(*args, url, **kwargs):
        some_function.url = 'https://www.' + url + '/'
        return some_function(*args, url=some_function.url, **kwargs)
    return wrapper

@some_decorator
def some_requests_dot_get_function(*args, url, **kwargs):
    return requests.get(*args, url=url, **kwargs).content.decode('utf-8')

some_requests_dot_get_function(url=url)

Not sure the exact use case you're intending, but maybe it could justify using some dict annotations for different functions to modify parameters depending on its particular item(s), that way at least you only have to modify this decorator rather than every function it decorates:

url = 'google.com'#'https://www.google.com/'
def some_decorator(some_function):
    def wrapper(*args, url, **kwargs):
        if some_function.__annotations__['return'].__getitem__('some_annotation') > 0:
            some_function.url = 'https://www.' + url + '/'
        else:
            some_function.url = url
        return some_function(*args, url=some_function.url, **kwargs)
    return wrapper

@some_decorator
def some_requests_dot_get_function(*args, url, **kwargs) -> {'some_annotation': 2}:
    return requests.get(*args, url=url, **kwargs).content.decode('utf-8')

some_requests_dot_get_function(url=url)

... or we could rely on the names of the functions to set the parameters in some_decorator:

url = 'google.com'#'https://www.google.com/'
def some_decorator(some_function):
    def wrapper(*args, url, **kwargs):
        if some_function.__name__.__contains__('some_requests'):
            some_function.url = 'https://www.' + url + '/'
        elif some_function.__name__.__contains__('some_other_requests'):
            some_function.url = 'https://www.' + url + '/?client=safari'
        else:
            some_function.url = url
        return some_function(*args, url=some_function.url, **kwargs)
    return wrapper

@some_decorator
def some_requests_dot_get_function(*args, url, **kwargs):
    return 'some: ' + requests.get(*args, url=url, **kwargs).content.decode('utf-8')

@some_decorator
def some_other_requests_dot_get_function(*args, url, **kwargs):
    return 'some other: ' + requests.get(*args, url=url, **kwargs).content.decode('utf-8')

some_requests_dot_get_function(url=url)
some_other_requests_dot_get_function(url=url)
Ori Yarden PhD
  • 1,287
  • 1
  • 4
  • 8
0

The following will adjust the behavior for methods that make a call to requests.get using the decorator my_custom_decorator. Given that changes to the source code directly or injecting a new method using exec with inspect and optparse was illegal.

There is one limitation, that is that a kwarg_ball has to be used to mediate the decorator and the requests.get method, as there is no other way for injecting additional *args, **kwargs.

import inspect
from functools import wraps


def my_custom_decorator(f):
    # Filter hotkeys access if there's currently some activity going on...
    @wraps(f)
    def wrap(*args, **kwargs):
        # Have to know ahead of time if there is going to be a call made, no way around it
        source_code = inspect.getsource(f)
        if 'requests.get' in source_code:
            requests.kwarg_ball = {'DATE': 'FOO', 'VAR': 'BAR'}
            result = f(*args, **kwargs)
            requests.kwarg_ball = None
        else:
            result = f(*args, **kwargs)
        return result

    return wrap


class requests:
    kwarg_ball = None

    @staticmethod
    def get(url):
        if requests.kwarg_ball is not None:
            print("Decorator modification detected!")
            for k, v in requests.kwarg_ball.items():
                print(k, ' - ', v)
        print(f'URL {url}')

The following produce the desire result with hello demonstrating the modified behavior, and goodbye executing normally.

@an_ugly_wrapper
def hello():
    requests.get('https://my-api-url')

def goodbye():
    requests.get('https://my-api-url')

hello()
goodbye()

Additionally, the kwarg_ball may be loaded into the **kwargs for the requests.get method.

Lucian
  • 95
  • 7
0

As you mentioned you don't want to change all occurrences, what you could do, is create a new file with name api.py (or whatever you like) and add all the function you want like this:

################## api.py ########################
import requests
def get(url, params=None):
    ..... You code goes here
    return request.get(url,params)
def post(url, params=None):
    ..... You code goes here
    return request.post(url,params)

Now, in your code files, just replace import requests with import api as requests

shekhar chander
  • 600
  • 8
  • 14