Spot any errors? let me know, but Unleash your pedant politely please.

Thursday, 1 May 2014

Dynamically adding instance methods.

I've been playing with some code today. It works, but may well be too clever for its own good. It may be slightly insane. There may be much better ways to do this. I'd written a little class (SingleCommandSFTP) to make some other code a little shorter and neater. With one command, it was no problem, but when it expanded to three, the duplication of calls to __open and __close irked a little, so I added __run_command. The pragmatic thing to do was to stop. But unfortunately, I was having fun, and wondered about dynamically adding instance methods to call __run_command for all the available commands. I ended up with this:
class SingleCommandSFTP(object):
    """
    SingleCommandSFTP is a simple abstraction of the the Paramiko
    SFTP client.   

    http://www.lag.net/paramiko/docs/paramiko.SFTPClient-class.html
    
    e.g.
        sftp_client = SingleCommandSFTP(host     = "hostname",
                                        username = "username",
                                        password = "password")

        sftp_client.remove(path = '/mnt/a/b/file.txt')
    
    It is intended mainly for single operations as the
    connection is opened and closed for each command.
    For multiple commands it will be inefficient.
    """
    
    def __init__(self,
                 host,
                 username,
                 password,
                 port = 22):

        self.host     = host
        self.username = username
        self.password = password
        self.port     = port
        
        
    def __open(self):
        self.transport = paramiko.Transport((self.host,
                                             self.port))
        self.transport.connect(username = self.username,
                               password = self.password) 
        self.sftp_client = paramiko.SFTPClient.from_transport(self.transport)

        
    def __close(self):
        self.sftp_client.close()
        self.transport.close()
        
    def __run_command(self,
                      command,
                      **params):
        
        """
        Call to a paramiko.SFTPClient.'command' instance method.
        """

        log.info('sftp_client.{command}({parameters})'.format(command    = command,
                                                  parameters = params))
        self.__open()
        method = getattr(self.sftp_client,command)
        method(**params)
        self.__close()



    # instance methods using __run_command for all instance methods of SFTPClient
    # are added once, dynamically, below. 
    # SFTPClient Reference can be found at http://www.lag.net/paramiko/docs/paramiko.SFTPClient-class.html
    

def instance_method_code_string(object,attr):
    argspec = inspect.getargspec(getattr(object,attr))
    parameters = [arg for arg in argspec.args]
    if argspec.defaults:
        for index in range(len(argspec.defaults)):
           try:
               parameters[-1-index]+='="' + argspec.defaults[-1-index] + '"'
           except:
               parameters[-1-index]+='=%s' %argspec.defaults[-1-index]
    if argspec.varargs:
        parameters.append['*'+argspec.varargs]
    if argspec.keywords:
        parameters.append['*'+argspec.keywords]

    code_string = "def {name}({parameters}):\n".format(name       = attr,
                                                       parameters = ','.join(parameters))
    code_string +="    self._SingleCommandSFTP__run_command(command='{name}',{parameters}"\
                  .format(name       = attr,
                          parameters = ','.join('{p}={p}'.format(p=p) for p in argspec.args[1:]))

    if argspec.varargs:
        code_string +='*'+argspec.varargs
    if argspec.keywords:
        code_string +='**'+argspec.keywords

    code_string +=")\n"
    return code_string
    
def add_SFTPClient_equivalent_instance_methods_to_SingleCommandSFTP():
    
    instance_method_names = [attr for attr in dir(paramiko.SFTPClient) if getattr(paramiko.SFTPClient,attr).__class__==paramiko.SFTPClient.__init__.__class__ and attr[0]!='_']
    
    for instance_method_name in instance_method_names:
        code_string = instance_method_code_string(object=paramiko.SFTPClient, attr=instance_method_name)
        exec(code_string)
        setattr(SingleCommandSFTP,instance_method_name, eval(instance_method_name))


if 'instance_methods_added' not in dir():
    instance_methods_added = True       
    add_SFTPClient_equivalent_instance_methods_to_SingleCommandSFTP()
    

getattr, setattr, eval, exec, getargspec. I won't have bloody clue what this does in two weeks' time!