4

I created a class for SOAP requests. I saw then that I'm doing same thing so I was writing the same methods with different logic and values. A similar example as below:

class Client(object):

    def get_identity(self, identity):
        context = {'identity': identity}
        return self.post_request('get_identity', context)

    def get_degree(self, degree, date):
        context = {
            'degree': degree,
            'date': date
        }
        return self.post_request('get_degree', context)

    def send_response(self, status, degree):
        context = {
            'degree': degree,
            'status': status,
            'reason': 'Lorem ipsum dolor sit amet.'
        }
        return self.post_request('send_response', context)

    def post_request(self, method, args):
        headers = "Creating headers"
        data = "Mapping hear with method and args arguments"

        response = requests.post(self.wsdl, data=data.encode('UTF-8'), headers=headers)

        root = objectify.fromstring(response.content)
        objectify.deannotate(root, cleanup_namespaces=True)
        return root

Then whichever I'm invoking a method:

client = Client()
self.client.get_identity(identity)

When I want to implement new SOAP method for example I'll write this:

def get_status(self, id):
    context = {'id': id}
    return self.post_request('get_status', context)

As I mentioned above my aim is to prevent this duplication and I have no idea how to do that.

vildhjarta
  • 574
  • 2
  • 5
  • 16
  • You could avoid hardcoding id, context, 'get_status' etc.. but the methods obviously do very different things so making a generic method that takes generic args or kwargs may make your code less readable – Padraic Cunningham Mar 22 '16 at 19:38
  • @PadraicCunningham i guess *args or **kwargs may be a nice way to increase code readability but it would not prevent duplication. – vildhjarta Mar 22 '16 at 19:52
  • You could make one function `get` that could do both what get_status and get_degree are doing but I would not change your code, there is no actual duplication as the methods are essentially very different in what they do – Padraic Cunningham Mar 22 '16 at 19:55
  • Ahead of try all answer I researched on real world examples via Github. It is similar to my class and implementation. Is this best practice or someting? This is not the single one [example](https://github.com/oanda/oandapy/blob/master/oandapy/oandapy.py) – vildhjarta Mar 23 '16 at 18:17
  • There is nothing wrong with your code, all you will be doing is making your code way less readable and usable by adopting hacks to try to save a few lines of code. There is no duplication in your code, because methods have a similar signature does not make them duplicates. I can read your code and know exactly what everything does. – Padraic Cunningham Mar 23 '16 at 18:20

3 Answers3

2

You could use decorators for this purpose. This way you can specify the methods in a very similar way you have been doing, but eliminate redundancy, while preserving documentation strings and argument specifications.

To build the context dictionary that you always create, with keys equal to parameter names, we do dict(zip(argspec, args)).

We decorate SOAP methods with a wrapper that calls post_request on self with the method name as first parameter and context as second.

Because you also want static parameters that are always added, we handle keyword arguments to the decorator, like described in this blog post by Carson Myers.

from decorator import decorator
from inspect import getargspec

def soap_method(f = None, **options):
  if f:
    @decorator
    def wrapper(method, client, *args):
      argspec = getargspec(method).args[1:]
      context = dict(zip(argspec, args))
      context.update(options)
      return client.post_request(method.__name__, context)
    return wrapper(f)
  else:
    return lambda f: soap_method(f, **options)

class Client:

  @soap_method
  def get_identity(self, identity):
    """Documentation works."""
    pass

  @soap_method
  def get_degree(self, degree, date):
    pass

  @soap_method(reason='Lorem ipsum dolor sit amet.')
  def send_response(self, status, degree):
    pass

  def post_request(self, method, context):
    print method, context

AFAIK you can't use docstrings with kindall's solution, while here:

>>> help(Client().get_identity)
Help on method get_identity in module foo:

get_identity(self, identity) method of foo.Client instance
    Documentation works.

Another advantage of using this approach is that if some methods need to modify the context dynamically, you can add these two lines to the wrapper:

dynamic = method(client, *args)
if dynamic: context.update(dynamic)

And you can define methods that compute things based on their arguments, rather than returning a static value:

@soap_method
def get_identity(self, identity):
  return {'hash': hash(identity)}

Finally, if you don't have the decorator module, then you can use this instead:

from functools import wraps
from inspect import getargspec

def soap_method(f = None, **options):
  if f:
    @wraps(f)
    def wrapper(client, *args):
      argspec = getargspec(f).args[1:]
      context = dict(zip(argspec, args))
      context.update(options)
      return client.post_request(f.__name__, context)
    return wrapper
  else:
    return lambda f: soap_method(f, **options)

This will still preserve docstrings, but won't preserve argspec, like decorator does.

arekolek
  • 9,128
  • 3
  • 58
  • 79
1

Write a function that writes the functions for you.

def req(methname, *names, **extra):

    def do(self, *values):
        context = dict(zip(names, values))
        context.update(extra)
        return self.post_request(methname, context)

    # create wrapper with proper signature
    f = eval("lambda self, %(p)s: do(self, %(p)s)" % {"p": ",".join(names)}, {"do": do})
    f.__name__ = methname

    return f

class Soapy(object):

    def post_request(self, method, args):
        headers = "Creating headers"
        data = "Mapping hear with method and args arguments"
        response = requests.post(self.wsdl, data=data.encode('UTF-8'), headers=headers)

        root = objectify.fromstring(response.content)
        objectify.deannotate(root, cleanup_namespaces=True)
        return root

class Client(Soapy):

    get_identity  = req("get_identity", "identity")
    get_degree    = req("get_degree", "degree", "date")
    send_response = req("send_response", "status", "degree", reason="Lorem ipsum")

The only remotely tricky part of this is getting the signatures to be useful if you do help() on them, which I did by evaluating a string containing a wrapper function. In Python 3.3+ you could instead build a signature object and do without the wrapper.

aneroid
  • 12,983
  • 3
  • 36
  • 66
kindall
  • 178,883
  • 35
  • 278
  • 309
1

This answer uses the partial method of functools and implements __getattr__. This returns a function when you say something like client.get_identity and then evaluates it using any additional arguments e.g. (status, degree).

from functools import partial
class Client(object):
    def __getattr__(self, attr):
        # This is called after normal attribute lookup fails
        # For this example, assume that means 'attr' should be a SOAP request
        return partial(self.post_request, attr)

    def post_request(self, method, **context):
        # Fill in with actual values
        headers = "Method %s"%method
        data = "arguments",context
        print "Header: %s\nData: %s"%(headers, data)

        response = requests.post(self.wsdl, data=data.encode('UTF-8'), headers=headers)
        root = objectify.fromstring(response.content)
        objectify.deannotate(root, cleanup_namespaces=True)
        return root

This can be used like:

client = Client()
client.get_identity(id="id", status="status")

Output:

Header: Method get_identity
Data: ('arguments', {'status': 'status', 'id': 'id'})
user812786
  • 4,302
  • 5
  • 38
  • 50
  • I would at least update `__getattr__` to check whether the name exists in `__dict__` and return it if so. That way actual instance variables could be accessed as well. – Jared Goguen Mar 23 '16 at 01:51
  • Actually, it was an [inaccurate point](http://stackoverflow.com/questions/3278077/difference-between-getattr-vs-getattribute). The `__getattr__` method is only called if the attribute is not in `__dict__`, so the added code is redundant. My bad, sorry about that. – Jared Goguen Mar 23 '16 at 15:57
  • `__getattr__` was my first thought as well, but it is not suitable IMO: currently it allows any method (e.g. `client.foo(bar='baz')`) - you could limit this by checking if attr is one of accepted methods and raising `AttributeError` otherwise. Then it's a bit better, but you still don't have argspecs, docstrings nor code completion. – arekolek Mar 24 '16 at 11:19
  • I agree with your other points (wrote this up kind of quickly and figured OP could fill in such details if they needed them), but what do you mean by not having code completion? – user812786 Mar 24 '16 at 13:15
  • AKA autocomplete, AKA tab completion, available in [IPython](http://ipython.readthedocs.org/en/stable/interactive/tutorial.html#tab-completion) and many [IDEs](http://stackoverflow.com/q/81584/1916449). Nevertheless, you might want to include the method name check and `AttributeError`, otherwise this answer is incomplete (it doesn't achieve the stated goal). BTW, to notify a previous commenter other than post author you need to mention their user name, e.g. `@arekolek`. – arekolek Mar 26 '16 at 08:37
  • @arekolek I know what IDE and incomplete mean, geez. I asked because didn't consider "autocomplete" to be a language feature. This code is a sketch to let you generate functions on the fly without having to explicitly write each. Having a predetermined list of acceptable functions is left as an exercise to the OP, who did not specify they needed all the things you listed, so I didn't see the need to include. You're welcome to expand on my answer if you want to implement those. – user812786 Mar 26 '16 at 12:56