Search code examples
pythonclassoophttpbottle

Why is this class and corresponding attribute not being destroyed between requests?


I can not understand this behavior at all. I asked a question yesterday or the day before thinking it was something with bottle.py, but after trying all kinds of possible solutions, even converting my app over to flask, I have pinpointed the behavior to a single, very simple, class, but I have NO idea why this is happening. It's confusing the hell out of me, but I would really like to understand this if anyone can please shed some light on it.

Ok, so first I have a class called Page which simplifies setting up templates a bit, this is the offending class:

class Page:
    """The page object constructs the webpage and holds associated variables and templates"""
    def __init__(self, template=None, name = '', title='',template_vars={}, stylesheets=[], javascript=[]):
        # Accepts template with or without .html extension, but must be added for lookup
        self.stylesheet_dir = '/css'
        self.javascript_dir = '/js'

        self.template = template
        self.template_vars = {}
        self.name = name
        self.title = title

        self.stylesheets = stylesheets
        self.javascript = javascript

        self.template_vars['debug'] = _Config.debug
        self.template_vars['title'] = self.title
        self.template_vars['stylesheets'] = self.stylesheets
        self.template_vars['javascript'] = self.javascript


    def render(self):
        """Should be called after the page has been constructed to render the template"""
        if not self.template.endswith(_Config.template_extension):
            self.template += '.' + _Config.template_extension

        if not self.title:
            if self.name:
                self.title = _Config.website + _Config.title_delimiter + self.name
            else:
                # If title not given use template name
                self.title = _Config.website + _Config.title_delimiter + self.template.rstrip('.html')

        try:
            template = env.get_template(self.template)
        except AttributeError:
            raise (AttributeError, 'template not set')

        rendered_page = template.render(self.template_vars)

        return rendered_page

    def add_stylesheet(self, name, directory=None):
        # Sanitize name
        if not name.endswith('.css'):
            name += '.css'
        if name.startswith('/'):
            name = name.lstrip('/')

        if not directory:
            directory = self.stylesheet_dir
        self.template_vars['stylesheets'].append(directory + '/' + name)

    def add_javascript(self, name, directory=None):
        # Sanitize name
        if not name.endswith('.js'):
            name += '.js'
        if name.startswith('/'):
            name = name.lstrip('/')

        if not directory:
            directory = self.javascript_dir
        self.template_vars['javascript'].append(directory + '/' + name)

And here is an example of a route that the problem is exhibited:

@route('/create_account', method=['GET','POST'])
def create_account():
    dbsession = db.Session()
    page = Page('create account')
    page.add_javascript('create_account.js')
    if request.method == 'GET':
        page.template = 'create_account'
        page.template_vars['status'] = None

        document = page.render()
        dbsession.close()
        return document

    elif request.method == 'POST':
        # Omitted

The problem lies with the method Page.add_javascript(). The next time I go to the /create_account page it is not creating a new Page object and instead reusing the old one. I know this because if I go to the page twice I will have two entires for the create_account.js in my returned html document(the template simply runs a for loop and creates a tag for any js files passed in that list). If I go 3 times it'll be listed 3 times, 40, 40 and so on. Now however if I simply change it to use the initializer and not the add_javascript method the problem goes away.

@route('/create_account', method=['GET','POST'])
def create_account():
    dbsession = db.Session()
    page = Page('create account', javascript=['create_account.js'])
    if request.method == 'GET':
        page.template = 'create_account'
        page.template_vars['status'] = None

        document = page.render()
        dbsession.close()
        return document

    elif request.method == 'POST':

However I suspect something is still wrong and just for my own sanity I need to understand what the hell is going on here. What is possibly happeneing behind the scenes where the add_javascript method would be called twice on the same page object? The method is called immediately after creating a new instance of the page object, where is it possibly getting the old contents of template_vars from?


Solution

  • The problem is that you use mutable defaults for your Page.__init__ function. See http://docs.python-guide.org/en/latest/writing/gotchas/#mutable-default-arguments.

    So you do get a new Page instance on each request, but the lists/dictionaries that hold your javascript etc are re-used.

    Replace list/dict default argument values with None, check for None in __init__.