0decimal0 0decimal0 - 1 month ago 11
Python Question

python- split() doesn't work inside __init__

I am writing code to serve a html file using wsgi.When I write a straight forward function I get no error like :

from wsgiref.simple_server import make_server
import os
...
...
def app(environ, start_response):
path_info = environ["PATH_INFO"]
resource = path_info.split("/")[1] #I get no error here the split works totally fine.


Now when I try to put the code inside a class I get error NoneType has no attribute split.
Perhaps the
environ
inside
__init__
doesn't get initialised , that's why it split returns nothing. Following is the file in which my class
Candy
resides :

import os
class Candy:
def __init__(self):
#self.environ = environ
#self.start = start_response
self.status = "200 OK"
self.headers = []

def __call__(self , environ , start_response):
self.environ = environ
self.start = start_response

#headers = []
def content_type(path):
if path.endswith(".css"):
return "text/css"
else:
return "text/html"


def app(self):
path_info = self.environ["PATH_INFO"]
resource = path_info.split("/")[1]

#headers = []
self.headers.append(("Content-Type", content_type(resource)))

if not resource:
resource = "login.html"
resp_file = os.path.join("static", resource)

try:
with open(resp_file, "r") as f:
resp_file = f.read()
except Exception:
self.start("404 Not Found", self.headers)
return ["404 Not Found"]

self.start("200 0K", self.headers)
return [resp_file]


Following is the
server.py
file where I invoke my make_server :

from wsgiref.simple_server import make_server
from candy import Candy
#from app import candy_request

candy_class = Candy()

httpd = make_server('localhost', 8000, candy_class.app)
print "Serving HTTP on port 8000..."

# Respond to requests until process is killed
httpd.serve_forever()

# Alternative: serve one request, then exit
#httpd.handle_request()


Any help ? How to get this error sorted and am I right in my assumption?

Answer Source

To explain what you're doing wrong here, let's start with simple concepts - what a WSGI application is.

WSGI application is just a callable that receives a request environment, and a callback function that starts a response (sends status line and headers back to user). Then, this callable must return one or more strings, that constitute the response body.

In the simplest form, that you have working it's just

def app(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")])
    return "hello, world"

make_server('localhost', 8000, app).serve_forever()

Whenever a request comes, app function gets called, it starts the response and returns a string (or it could return an iterable of multiple strings, e.g. ["hello, ", "world"])

Now, if you want it to be a class, it works like this:

 class MyApp(object):
     def __init__(self):
         pass

     def __call__(self, environ, start_response):
         start_response("200 OK", [("Content-Type", "text/plain")])
         return "something"

 app = MyApp()
 make_server("localhost", 8000, app).serve_forever()

In this case, the callable is app, and it's actually __call__ method of Caddy class instance.

When request comes, app.__call__ gets called (__call__ is the magic method that turns your class instance in a callable), and otherwise it works exactly the same as the app function from the first example. Except that you have a class instance (with self), so you can do some pre-configuration in the __init__ method. Without doing anything in __init__ it's useless. E.g., a more realistic example would be this:

 class MyApp(object):
     def __init__(self):
         self.favorite_color = "blue"

     def __call__(self, environ, start_response):
         start_response("200 OK", [("Content-Type", "text/plain")])
         return "my favorite color is {}".format(self.favorite_color)
 ...

Then, there's another thing. Sometimes you want a streaming response, generated over time. Maybe it's big, or maybe it takes a while. That's why WSGI applications can return an iterable, rather than just a string.

def app(environ, start_response):
    start_response("200 OK", [("Content-Type", "text/plain")]))
    yield "This was a triumph\n"
    time.sleep(1)
    yield "I'm making a note here\n"
    time.sleep(1)
    yield "HUGE SUCCESS\n"

make_server("localhost", 8000, app).serve_forever()

This function returns a generator that returns text, piece by piece. Although your browser may not always show it like this, but try running curl http://localhost:8000/.

Now, the same with classes would be:

class MyApp(object):
    def __init__(self, environ, start_response):
        self.environ = environ
        self.start = start_response

    def __iter__(self):
        self.start("200 OK", [("Content-Type", "text/plain")]))
        yield "This was a triumph\n"
        time.sleep(1)
        yield "I'm making a note here\n"
        time.sleep(1)
        yield "HUGE SUCCESS\n"

make_server("localhost", 8000, MyApp).serve_forever()

Here, you pass MyApp (the class) as a application callable - which it is. When request comes it gets called, it's like someone had written MyApp(environ, start_response) somewhere, so __init__ starts and creates an instance for this specific request. Then, as the instance is iterated, __iter__ starts to produce a response. After it's done, the instance is discarded.

Basically, that's it. Classes here are only convenience closures that hold data. If you don't need them, don't use classes, use plain functions - flat is better than nested.


Now, about your code.

What your code uses for callable is Candy().app. This doesn't work because it doesn't even made to receive environ and start_response it will be passed. It should probably fail with 500 error, saying something like app() takes 1 positional arguments but 3 were given.

I assume the code in your question is modified after you got that NoneType has no attribute split issue, and you had passed something to the __init__ when creating candy_instance = Candy() when your __init__ still had 2 arguments (3 with self). Not even sure what exactly it was - it should've failed earlier.

Basically, you passed the wrong objects to make_server and your class was a mix of two different ideas.

I suggest to check my examples above (and read PEP-333), decide what you actually need, and structure your Candy class like that.

  • If you just need to return something on every request, and you don't have a persistent state - you don't need a class at all.

  • If you need a persistent state (config, or, maybe, a database connection) - use a class instance, with __call__ method, and make that __call__ return the response.

  • If you need to respond in chunks, use either a generator function, or a class with __iter__ method. Or a class with __call__ that yields (just like a function).

Hope this helps.