Mixin
Mixins are a way to provide implementation details that simplify the work for a developer to conform to an opaque interface specification. In this module, we have one request handler interface and two request handler mixins to illustrate how mixins can make our lives easier when defining concrete classes.
from abc import ABC, abstractmethod
from dataclasses import dataclass
@dataclass
class Request:
"""Request data model.
Assumes only GET requests for simplicity so there is no method
attribute associated with this class.
"""
url: str
user: str
class RequestHandler(ABC):
"""Request handler interface.
In the real world, a URL is expected to handle different kinds of HTTP
methods. To support this, we would define a `View` class with a method
that dispatches the request payload to the appropriate HTTP handler.
Afterwards, we would register the class to a URL router. Check out
the source code in Django and Flask to see how that works:
https://github.com/django/django
https://github.com/pallets/flask
"""
@abstractmethod
def handle(self, request):
"""Handle incoming request."""
raise NotImplementedError
class TemplateHandlerMixin(RequestHandler):
"""Abstract template mixin for RequestHandler.
This is a mixin because it's an abstract class that provides a
concrete implementation of the `handle` method. In other words, it
adds additional structure for implementing the `handle` routine. This
class helps if downstream developers typically implement request
handlers that retrieve template content.
"""
template_suffix = ".template"
def handle(self, request):
template_name = self.get_template_name(request.url)
if not self.is_valid_template(template_name):
return self.handle_invalid_template(request)
return self.render_template(template_name)
@abstractmethod
def get_template_name(self, request_url):
"""Get template name."""
raise NotImplementedError
def is_valid_template(self, template_name):
"""Check if template name is valid."""
return template_name.endswith(self.template_suffix)
@staticmethod
def handle_invalid_template(request):
"""Handle request for invalid template."""
return f"<p>Invalid entry for {request.url}</p>"
@abstractmethod
def render_template(self, template_name):
"""Render contents of specified template name."""
raise NotImplementedError
class AuthHandlerMixin(RequestHandler):
"""Abstract auth mixin for RequestHandler.
This is another mixin class where authentication helper methods are
defined in this abstract class and must be implemented in concrete
classes. Notice that the `handle` method is implemented and returns
the output of the parent's `handle` method. This means that we can
continue to chain mixin effects as long as this mixin is to the left
of another mixin in a concrete class MRO.
"""
def handle(self, request):
if not self.is_valid_user(request.user):
return self.handle_invalid_user(request)
return super().handle(request)
@abstractmethod
def is_valid_user(self, request_user):
"""Check if user is valid."""
raise NotImplementedError
@staticmethod
def handle_invalid_user(request):
"""Handle request for invalid user."""
return f"<p>Access denied for {request.url}</p>"
class TemplateFolderHandler(TemplateHandlerMixin):
"""Concrete template handler.
This concrete class implements the abstract methods of its parent
mixin. By extension, it has implemented everything that is needed
for the `handle` method.
"""
def __init__(self, template_dir):
self.template_dir = template_dir
def get_template_name(self, request_url):
return request_url[1:]
def is_valid_template(self, template_name):
return (super().is_valid_template(template_name)
and template_name in self.template_dir)
def render_template(self, template_name):
return self.template_dir[template_name]
class AdminTemplateHandler(AuthHandlerMixin, TemplateFolderHandler):
"""Concrete auth and template handler.
This concrete class gets the benefits of the previous concrete
class but also gets authentication for free just by implementing
the abstract method of the authentication mixin.
"""
def __init__(self, admin_users, template_dir):
super().__init__(template_dir)
self.admin_users = admin_users
def is_valid_user(self, request_user):
return request_user in self.admin_users
def main():
# Handle requests with simple template handler
simple_dir = {"welcome.template": "<p>Hello world</p>",
"about.template": "<p>About me</p>"}
simple_handler = TemplateFolderHandler(simple_dir)
welcome_from_nobody = Request("/welcome.template", "nobody")
about_from_nobody = Request("/about.template", "nobody")
foo_from_nobody = Request("/foo.bar", "nobody")
assert simple_handler.handle(welcome_from_nobody) == "<p>Hello world</p>"
assert simple_handler.handle(about_from_nobody) == "<p>About me</p>"
assert simple_handler.handle(foo_from_nobody) == "<p>Invalid entry for /foo.bar</p>"
# Handle requests with admin template handler
admin_users = {"john", "jane"}
admin_dir = {"fqdn.template": "<p>server.example.com</p>",
"salary.template": "<p>123456789.00</p>"}
admin_handler = AdminTemplateHandler(admin_users, admin_dir)
fqdn_from_john = Request("/fqdn.template", "john")
salary_from_jane = Request("/salary.template", "jane")
salary_from_nobody = Request("/salary.template", "nobody")
foo_from_john = Request("/foo.bar", "john")
assert admin_handler.handle(fqdn_from_john) == "<p>server.example.com</p>"
assert admin_handler.handle(salary_from_jane) == "<p>123456789.00</p>"
assert admin_handler.handle(salary_from_nobody) == "<p>Access denied for /salary.template</p>"
assert admin_handler.handle(foo_from_john) == "<p>Invalid entry for /foo.bar</p>"
if __name__ == "__main__":
main()