A side-by-side Comparison of Django and Moya

The Django code in this post comes from the official Django tutorial.

I've tried not to be disingenuous with the comparison, and I'm only going to compare like with like, so I can show code from both frameworks and let you draw your own conclusions. I'll cover the areas where they differ in another post.

Models

Both Moya and Django use models to map databases on to familiar data structures. In the case of Moya, the mapping is done with SQLAlchemy. Django uses its own ORM.

Here's the models.py from the Django tutorial and a Moya version:

models.py

import datetime from django.db import models from django.utils import timezone class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField('date published') def __str__(self): return self.question_text def was_published_recently(self): return self.pub_date >= timezone.now() - datetime.timedelta(days=1) class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) def __str__(self): return self.choice_text

models.xml

<moya> <model libname="Question" repr="question_text" xmlns="http://moyaproject.com/db"> <string name="question_text" length="200"/> <datetime name="pub_date" label="date published"/> <property name="was_published_recently" expression="pub_date gte .now - 1d"/> </model> <model libname="Choice" repr="choice_text" xmlns="http://moyaproject.com/db"> <foreign-key name="question" model="#Question" backref="choices" owned="yes"/> <string name="choice_text" length="200"/> <integer name="votes" default="0"/> </model> </moya>

You may have noticed that there is nothing comparable to Python's import statements in the Moya code. No imports means there are no circular imports, and the lack of ambiguity in Moya tags means that there is no equivalence of needing to know if a file does "import datetime" or "from datetime import datetime" (for instance).

The naming and syntax may vary but the same information is present in both files. Where they differ is that the Python code has a __str__ method and the Moya code has a repr attribute (actually an expression) that do the same thing.

Another difference is the property was_published_recently which is calculated with the following expressions:

Django / Python

self.pub_date >= timezone.now() - datetime.timedelta(days=1)

Moya / XML

pub_date gte .now - 1d

The reason the Moya expression uses gte rater then >= is that greater than symbols must be escaped in XML, and >= is rather ugly.

URLS

Next up is the urls file which maps URLs on to the code that will generate a response.

Same as before, Django first, then Moya.

urls.py

from django.conf.urls import url from . import views app_name = 'polls' urlpatterns = [ url(r'^$', views.IndexView.as_view(), name='index'), url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'), url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'), url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'), ]

urls.xml

<moya> <mountpount> <url route="/" view="#view.index" name="index"/> <url route="/{posinteger:pk}/" view="#view.detail" name="detail"/> <url route="/{posinteger:pk}/results/" view="#view.results" name="results"/> <url route="/{posinteger:pk}/vote/" view="#view.vote" name="vote"/> </mountpoint> </moya>

Other than the lack of imports, the Moya code has a similar layout to the Django code. Worthy of note is that Moya favours a simpler (and arguably more readable) syntax for parsing URLs, over regular expressions.

In the Django code, the views are specified with by a callable expression, whereas the Moya code uses an attribute containing a string such as "#view.detail". This attribute is an element reference which tells Moya to look for the view with a libname attribute of view.detail (example views below).

An advantage of Moya's method of referencing views is that it is independent of path and module hierarchy; if you were move the view to another file, or directory, nothing would break. A potential boon for refactoring.

Views

The views are the meat and veg of writing web applications. Most custom functionality will take place within a view. Here is the views file from the tutorial and a Moya version.

views.py

from django.shortcuts import get_object_or_404, render from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse from django.views import generic from .models import Choice, Question class IndexView(generic.ListView): template_name = 'polls/index.html' context_object_name = 'latest_question_list' def get_queryset(self): return Question.objects.order_by('-pub_date')[:5] class DetailView(generic.DetailView): model = Question template_name = 'polls/detail.html' class ResultsView(generic.DetailView): model = Question template_name = 'polls/results.html' def vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: selected_choice = question.choice_set.get(pk=request.POST['choice']) except (KeyError, Choice.DoesNotExist): return render(request, 'polls/detail.html', { 'question': question, 'error_message': "You didn't select a choice.", }) else: selected_choice.votes += 1 selected_choice.save() return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

views.xml

<moya xmlns:db="http://moyaproject.com/db" xmlns:let="http://moyaproject.com/let"> <view libname="view.index" template="polls/index.html"> <db:query model="#Question" orderby="-pub_date" maxresults="5" dst="latest_question_list"/> </view> <view libname="view.detail" template="polls/detail.html"/> <db:get-required model="#Question" let:id=".url.pk" dst="question"/> </view> <view libname="view.results" template="polls/results.html"> <db:get-required model="#Question" let:id=".url.pk" dst="question"/> </view> <view libname="view.vote" template="polls/detail.html"> <db:get-required model="#Question" dst="question" let:id=".url.question_id"/> <db:get model="#Choice" src="question.choices" let:id=".request.POST.choice" dst="selected_choice"> <inc src="selected_choice.votes"/> <redirect name="results" let:question_id="question.id"/> </db:get> <str dst="error_message">You didn't select a choice.</str> </view> </moya>

Of the four views in the tutorial, three of them are Django's generic views. Moya doesn't really have an analogue of generic views, although for this tutorial the regular Moya views above are equivalent.

The last view is more interesting for the purposes of this post. It has essentially the same function as the Python code. Without turning this in to a Moya tutorial, the tags do the following:

<db:get-required> gets a database object, or raises a 404 if it isn't found

gets a database object, or raises a 404 if it isn't found <db:get> Gets a database object and executes the enclosed block if it exists, otherwise it skips the enclosed block

Gets a database object and executes the enclosed block if it exists, otherwise it skips the enclosed block <inc> Increments a value

Increments a value <redirect> Redirects to a named url

Redirects to a named url <str> sets a string

Templates

It's also worth comparing templates from both frameworks. The following templates render a form:

detail.html (Django)

<h1>{{ question.question_text }}</h1> {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} <form action="{% url 'polls:vote' question.id %}" method="post"> {% csrf_token %} {% for choice in question.choice_set.all %} <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" /> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br /> {% endfor %} <input type="submit" value="Vote" /> </form>

detail.html (Moya)

<h1>${question.question_text}</h1> {% if error_message %}<p><strong>${error_message}</strong></p>{% endif %} <form action="{% url 'vote' with question_id=question.id %}" method="post"> {%- for counter, choice in enumerate1:question.choices %} <input type="radio" name="choice" id="choice${counter}" value="${choice.id}" /> <label for="choice${counter}">${choice.choice_text}</label><br /> {%- endfor %} <input type="submit" value="Vote" /> </form>

You would have to look closely to see the differences beyond the syntax details (Moya prefers ${} over {{}} for substitution). The Moya template lacks a {% csrf_token %} tag as Moya's cross site request forgery protection is handled by a forms library, which the tutorial code doesn't have a direct comparison for. If we were using the forms library, and we wanted to render the form manually, this is how we could add the CSRF token:

<input type="hidden" name="_moya_csrf" value="${form.csrf_token}"/>

Another difference is that the Moya version creates a counter value explicitly with the expression enumerate1:question.choices , as there is no implicit forloop value.

Finally, the url tag is different in Moya; it only accepts keyword arguments, whereas Django's url tag accepts both positional and keyword arguments. There is also no need to specify the application in Moya, as it is assumed to be from the same application that is rendering the template.

I would argue that it is better to use keyword arguments in url tags, even with Django, as its more self-documenting and less likely to break if you re-arrange your url patterns.

Generally though, the template systems have a similar look and feel. Moya is around 30% faster -- not that template speed is much of an issue these days, and Jinja is faster than both Moya and Django templates. Where I do think Moya wins is that it has full expressions, and the error reporting is more advanced.

Apples

For the apples-to-apples code, Moya is comparable to Django and similar web frameworks in terms of lines of code. Not that number of keypresses is a good metric for any technology comparison, but the Moya doesn't suffer from the bloat often associated with XML.

Where Moya does save on typing is that a tag is a self contained little operation, independent of its context in the file. Take for instance, a redirect. The following lines from the above code are all involved in a redirect:

from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

There's nothing complex in these lines, but there is a tiny mental burden that can slow you down. By mental burden, I mean things like struggling to remember the import for 'reverse' or how HttpResponseRedirect is capitalized (should it not be HTTPResponseRedirect)? There are also a few things to go wrong; you may forget the comma to make the args parameter a tuple or not notice you're a brace short at the end. Such mistakes are easily noticed and fixed, but I find these things are 'a death of a thousand cuts' for development speed.

In Moya, the redirect is done as follows:

<redirect name="results" let:question_id="question.id"/>

The complexity is hidden behind a relatively memorable tag interface. There are many such tags that cover a broad spectrum of web development tasks.

Non Apples

There are some features of Moya that aren't directly comparable with other Python frameworks. Such as content which describes a page with high level components. These components (or widgets) are code and template wrapped in a simple tag interface.

Lets look at a quick example of content. The following renders 'post.html' for each item in a query set of posts:

<for src="posts" dst="post"> <node template="post.html" let:post="post"/> </form>

Here's how we would modify the above to paginate the posts in to a maximum of 10 items per page:

<w:paginate src="posts" dst="post" auto="yes"> <node template="post.html" let:post="post"/> </w:paginate>

This single change enables the pagination used here blog (including the HTML). Such widgets are re-usable across projects and trivial to customize.

Curious?

If you would like to have a look at more Moya code, a good place to start would be Moya Techblog, which powers this blog. In particular, this directory, which does the heavy lifting.