How to pass arguments to TCPServer handler class

55 Views Asked by At

Here's a piece of experimental/educational code written solely to "play" with socketserver functionality.

The code works without error. All it does is open and read a file sending the file's contents over a TCP connection to a TCPServer. The server prints the received data during the overridden handle() function of BaseRequestHandler.

from socket import socket, AF_INET, SOCK_STREAM
from socketserver import BaseRequestHandler, TCPServer
from threading import Thread

HOST = "localhost"
PORT = 10101
ADDR = HOST, PORT
RECVBUF = 4096
SENDBUF = RECVBUF // 2
INPFILE = "inputfile.txt"


class MyHandler(BaseRequestHandler):
    def handle(self):
        while data := self.request.recv(RECVBUF):
            print(data.decode(), end="")


def server(tcpserver: TCPServer):
    tcpserver.serve_forever()


if __name__ == "__main__":
    with open(INPFILE, "rb") as indata:
        with TCPServer(ADDR, MyHandler) as tcpserver:
            (t := Thread(target=server, args=[tcpserver])).start()
            with socket(AF_INET, SOCK_STREAM) as s:
                s.connect(ADDR)
                while chunk := indata.read(SENDBUF):
                    s.sendall(chunk)
            tcpserver.shutdown()
            t.join()

So this is fine but what if I want the handle() function to write the received data to a file? Sure, I could hard-code the filename into the handle() function or maybe even make it globally available. Neither of those options seem particularly Pythonic to me.

The TCPServer is constructed based on a RequestHandler type - i.e., not a class instance. What I really want to be able to do is (somehow) pass a filename into the MyHandler class. The TCPServer instance will (presumably) have an internal instance variable for the constructed MyHandler but that's not documented so I don't know where that is.

Maybe I'm missing something fundamental but I just can't figure out the "right" way to do achieve this

2

There are 2 best solutions below

0
Nikolaj Š. On BEST ANSWER

One way of getting around this API limitation is to use class factory function:

def get_handler(fname):
    class MyHandler(BaseRequestHandler):
        def handle(self):
            while data := self.request.recv(RECVBUF):
                print(data.decode(), end="")
                # copy it to file, I dunno =)
                with open(fname, 'wb') as f:
                    f.write(data)
    return MyHandler

Then use it like that:

    with TCPServer(ADDR, get_handler('/tmp/file.bin')) as tcpserver:
0
Nikolaj Š. On

Another possible option would be to take a look into internal socket module API and subclass TCPServer to override finish_request() method:

class MyTCPServer(TCPServer):
    finish_request(self, request, client_address):
        """ That's the only method where handler is used:
            could be easily overridden
        """
        self.RequestHandlerClass(request, client_address, self)

There's a lot of ways to go from here:

  • introduce something like MyTCPServer.filename attribute and pass it as another constructor argument to RequestHandlerClass
  • replace handler class instatiation with function call or callable class instance call
  • maybe override __init__() to essentially rename self.RequestHandlerClass to be more explicit if it's not treated like a class anymore

Overall, BaseRequestHandler API is quite simple and easy to extend.