4
\$\begingroup\$

I am playing around with building my own back end with python 2.7 and jinja2/cgi. Originally I was defining some of my environment and loader variables at the top but figured this was not the best practice. I then defined functions for each, but they'd only get called once or twice depending on which route it decided to execute. I came up with this class idea instead that I pass into my dispatch function, which in turn passes it into whatever rendering function happens to be being executed. Is this the proper 'pythonic' way to approach having a template loader, and if not would anyone have any suggestions?

class Template(object):
    def __init__(self):
        self._environment = None

    def __call__(self, temp_name):
        return self.get_env.get_template(temp_name)

    @property
    def get_env(self):
        return self._environment

    @get_env.getter
    def get_env(self):
        if not self._environment:
            self._loader = jinja2.FileSystemLoader(searchpath=os.path.join(
              os.path.dirname(os.path.abspath(__file__)), '../templates/madlib/'))
            self._environment = jinja2.Environment(loader = self._loader)

        return self._environment

This class then allows me to just call template('templateFileName.j2') and be returned that jinja template object. If the environment is not created, the getter will create one for me.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

For starter, I'd rewrite get_env a bit and introduce intermediate variables for readability:

def get_env(self):
    if not self._environment:
        cwd = os.path.dirname(os.path.abspath(__file__))
        madlib_dir = os.path.join(cwd, '..', 'templates', 'madlib')
        loader = jinja2.FileSystemLoader(searchpath=madlib_dir)
        self._environment = jinja2.Environment(loader=loader)
    return self._environment

Next I would cleanup your @property usage; since the raw decorator already defines the getter property, you can get rid of the first method:

class Template(object):
    def __init__(self):
        self._environment = None

    def __call__(self, temp_name):
        return self.get_env.get_template(temp_name)

    @property
    def get_env(self):
        if not self._environment:
            cwd = os.path.dirname(os.path.abspath(__file__))
            madlib_dir = os.path.join(cwd, '..', 'templates', 'madlib')
            loader = jinja2.FileSystemLoader(searchpath=madlib_dir)
            self._environment = jinja2.Environment(loader=loader)
        return self._environment

Lastly, since the initialization of _environment is meant to be done (and thus checked) only once, why not take advantage of attribute access that already offer us this kind of flexibility:

class Template(object):
    def __call__(self, temp_name):
        return self.environment.get_template(temp_name)

    def __getattr__(self, attribute):
        if attribute == 'environment':
            cwd = os.path.dirname(os.path.abspath(__file__))
            madlib_dir = os.path.join(cwd, '..', 'templates', 'madlib')
            loader = jinja2.FileSystemLoader(searchpath=madlib_dir)
            self.environment = env = jinja2.Environment(loader=loader)
            return env
        return super(Template, self).__getattr__(attribute)

This way, __getattr__ is only called the first time self.environment is accessed and never again. But... Hold on... The only usefull method of this class is __call__... Better write a function instead; this will bring the check back at each call but will make the intent clearer:

def load_template(template_name):
    if not hasattr(load_template, 'environment'):
        cwd = os.path.dirname(os.path.abspath(__file__))
        madlib_dir = os.path.join(cwd, '..', 'templates', 'madlib')
        loader = jinja2.FileSystemLoader(searchpath=madlib_dir)
        load_template.environment = jinja2.Environment(loader=loader)
   return load_template.environment.get_template(template_name)

So now, no need to pass an object around everywhere, just call the function when you need to.

And if you want to, EAFP can also help reduce the overhead of the hasattr test:

def load_template(template_name):
    try:
        environment = load_template.environment
    except AttributeError:
        cwd = os.path.dirname(os.path.abspath(__file__))
        madlib_dir = os.path.join(cwd, '..', 'templates', 'madlib')
        loader = jinja2.FileSystemLoader(searchpath=madlib_dir)
        load_template.environment = environment = jinja2.Environment(loader=loader)
   return environment.get_template(template_name)
\$\endgroup\$
5
  • \$\begingroup\$ I agree with this when looking at as a standalone example and that is essentially what I was working with before switching over to a property/class based data structure. I am shooting for a singleton design as I work towards multi-threaded processes down the road \$\endgroup\$
    – Mike
    Commented Mar 6, 2017 at 20:34
  • 1
    \$\begingroup\$ @Turk Even with your approach you'll get the same kind of race condition when accessing the attribute the first times than calling the function the first times. The solution is not a switch of data model but a lock + second check. \$\endgroup\$ Commented Mar 6, 2017 at 21:50
  • \$\begingroup\$ Ah okay, is that lock atomic by chance? I am just getting into these new concepts of running multiple invocations of a script in serial/parallel or both. I thought my approach would be sound in that regard, but the more I look at your answer the more I agree with it \$\endgroup\$
    – Mike
    Commented Mar 6, 2017 at 22:03
  • 1
    \$\begingroup\$ @Turk Not sure about what you are trying to do. Running multiple instances of python (even in parallel) is not at all the same thing than running several threads into the same script. You don't have to consider concurency in the first case as each invocation will have to initialize an environment anyway; but in the second case you might need it. The lock I was thinking about comes from the threading module and is not really needed if you don't use anything from it. \$\endgroup\$ Commented Mar 6, 2017 at 22:10
  • \$\begingroup\$ Okay that makes more sense, I thought there was some relation between the two but that clears things up. I am not trying to do anything specifically but gain an understanding of the different ways a script could be ran and how that effects global attributes like request data/jinja environment and things of that sort that all my rendering functions require. I was hoping that having an understanding of the different ways the script can be executed (cgi/wsgi being the primary ways) will allow me to develop my back end in a more 'correct' way. \$\endgroup\$
    – Mike
    Commented Mar 6, 2017 at 23:11

Not the answer you're looking for? Browse other questions tagged or ask your own question.