1
\$\begingroup\$

I am using an object that expects a keyword argument for every call, but I never intend to change it. In this case a timeout of 10. What is the best way to add a partialmethods for each attributes.

In my example I want to set a timeout for my session requests http methods.

I could monkeypatch Session.request with a partialmethod. This works fairly well because all of the session methods go through request. Session().post is actually going through Session.request('post', <inputs>).

from requests import Session

Session.request = partialmethod(Session.request, timeout=10)

However, what about use cases where there isn't a single method I can fix or I simply don't want to do something potentially dangerous like monkeypatching.

I have a session helper object that configures the auth and the adapter (omitted here) for the client, but I want different default timeouts for different methods. I can't even monkeypatch the method in that case.

I could do it after standard definition of the class in a loop.

from functools import partialmethod
from requests import Session

class SeshTester:
    def __init__(self):
        self.s = Session()

    def request(self, method, *args, **kwargs):
        return self.s.request(method, *args, **kwargs)


for r_method in ['delete', 'get', 'head', 'options']:
    setattr(SeshTester, r_method, partialmethod(SeshTester.request, r_method, timeout=7))

for r_method in ['connect', 'put', 'push']:
    setattr(SeshTester, r_method, partialmethod(SeshTester.request, r_method, timeout=13))

# this call is equivalent to self.s.request('get', 'http://google.com', timeout=7
print(SeshTester().get('http://google.com'))

I don't think this is very legible, but am unaware how to do this inside of standard class definition. SeshTester object obviously isn't reference-able before it is made, but I don't want to repeat myself by doing post = partialmethod(request, 'post', timeout=13) for every possible http method.

I would like to perform this loop in the class, but don't know how to reference the object before it's fully defined.

\$\endgroup\$
  • 1
    \$\begingroup\$ This doesn't actually look too bad, my only recommendation would be to allow a session to be passed into the constructor of SeshTester (and maybe name that better?) for testing purposes. \$\endgroup\$ – Bailey Parker Jan 25 '18 at 8:33
  • \$\begingroup\$ The client for this code requires header based authentication and tls1.2. So I have a setup_session method that mounts Tls12HttpAdapter to to the base url, and attaches <Company>Auth as well. \$\endgroup\$ – Brian Jan 25 '18 at 8:47
  • \$\begingroup\$ Sorry, missed the edit window. The real class is '<company>APIHelper'. Considering the above strict session requirements I believe many users would accidentally pass in sessions that can't actually connect to the product. I figured if needed a dev can just setattr the session. The non-experienced intended audience is why I was hoping there was a more legible way to do the above. \$\endgroup\$ – Brian Jan 25 '18 at 8:53
2
\$\begingroup\$

It is certainly possible to do what was outlined that lies outside the class definition within the class definition itself through the usage of locals, and with that the intent of what is to be done can be expressed more clearly with less verbose code and lookup using setattr.

class SeshTester:
    def __init__(self):
        self.s = Session()

    def request(self, method, *args, **kwargs):
        return self.s.request(method, *args, **kwargs)

    for r_method in ['delete', 'get', 'head', 'options']:
        locals()[r_method] = partialmethod(request, r_method, timeout=7)

    for r_method in ['connect', 'put', 'push']:
        locals()[r_method] = partialmethod(request, r_method, timeout=13)

Seeing this working as intended from the shell:

>>> SeshTester().get
functools.partial(<bound method SeshTester.request of <__main__.SeshTester object at 0x7fde00176ef0>>, 'get', timeout=7)
>>> SeshTester().put
functools.partial(<bound method SeshTester.request of <__main__.SeshTester object at 0x7fddf8da0668>>, 'put', timeout=13)

One more thing: the documentation linked above does not actually describe what is implemented, at least for CPython; other implementations may have locals() behave differently within the class scope, however there is now an effort to properly standardize this underspecified behavior to the CPython implementation in PEP-558, such that the above code example will work as intended according to the standard (if the PEP gets ratified into Python 3.7).


However, as mentioned in the question, if the API provides a standardized helper method that provides a specific Session instance, certainly it can provide a subclassed Session instance with the specific features/methods that require overriding be applied where needed. Consider the following:

class SessionExtras(Session):

    def request(self, method, url, *a, **kw):
        default_timeout = 7 
        timeouts = {
            'CONNECT': 13,
            'PUT': 13,
            'PUSH': 13,
        } 
        if kw.get('timeout') is None:
            kw['timeout'] = timeouts.get(method, default_timeout)
        return super().request(method, url, *a, **kw)

Perhaps this way the intent of what is to be done is expressed in a much more clear way, and allow the API users to make use of a real Session instance rather than some surrogate. For this example, the default_timeout and timeouts is defined within the function definition, however they could conceivably be moved to the class or even module level, depending on the intended usage and how it is to be customized for the specific API that is being written.

\$\endgroup\$
  • \$\begingroup\$ So, will PEP558 change the safety of changing locals. According to the documentation, "The contents of this dictionary should not be modified; changes may not affect the values of local and free variables used by the interpreter". Is that warning true at every scope? I haven't been able to find a good explanation of it yet. \$\endgroup\$ – Brian Feb 3 '18 at 17:02
  • \$\begingroup\$ @Brian As the intent of that PEP is to fully document the current CPython behaviour, it should be maintained (note its abstract). Regardless, tests should be written if you wish to use the short-hand, or alternatively you can go full on and create a metaclass but it is a way too heavy method when an alternative can be provided (the second part of the answer). \$\endgroup\$ – metatoaster Feb 3 '18 at 19:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.