You know the data is there, let's make it easier to get it
Published on May/20
Honestly, sometimes I get tired of having to write "boilerplatish" code repeatedly in my projects because they are needed in to achieve very simple algorithm tasks that are used multiple times during development.
Examples of that involve retrieving and using: URL arguments, GET
parameters and cleaned_data
from a form.
1class MyForm(Form):
2
3 # Your form goes here
4 pass
5
6class MyView(FormView):
7
8 form_class = MyForm
9
10 def get(self, request, *args, **kwargs):
11 arg_1 = self.kwargs['arg_1']
12 arg_2 = self.kwargs['arg_2']
13 # Default value
14 param = 10
15 if 'param' in self.GET:
16 param = self.GET['param']
17 var_x = arg_1 * arg_2
18 self.some_func(var_x, param)
19
20 def form_valid(self, form):
21 field_1 = form.cleaned_data['field_1']
22 field_2 = form.cleaned_data['field_2']
23 field_3 = form.cleaned_data['field_3']
24 var_1 = field_1 + field_2
25 var_2 = field_1 * field_3
26 self.other_func(var_1, var_2)
It is already known that arguments come from kwargs
, parameters come from request.GET
and form data comes from form.cleaned_data
. Why is it necessary to write all of that everytime those structures are used?
Looking for ways to improve that I learned about Python's __getattr__
function and created a simple class that can be helpful in these situations and many others.
1class AttrProviderMixin(object):
2
3 is_providing = False
4
5 def can_provide_attr(self, attr):
6 # Validate in the class' attributes that it's possible to retrieve the
7 # expected dynamic attribute
8 return False
9
10 def provide_attr(self, attr):
11 # Retrieve the value of the requested dynamic attribute
12 return None
13
14 def default_for_attr(self, attr):
15 # Define a default value for the attribute
16 return None
17
18 def __getattr__(self, item):
19 """ Intercept an invalid attribute check to provide the dynamic value. """
20 try:
21 return super().__getattr__(item)
22 except AttributeError:
23 # Try to detect an attribute validation loop and avoid it by
24 # raising the error of an invalid attribute
25 if self.is_providing:
26 self.is_providing = False
27 raise
28 # Mark that it's trying to retrieve the attribute to avoid loops
29 self.is_providing = True
30 # Subclass can provide a default value
31 default = self.default_for_attr(item)
32 # If it's an unexpected attribute with no default value, the
33 # dynamic attribute is not valid in this class
34 if not self.can_provide_attr(item) and default is None:
35 self.is_providing = False
36 raise AttributeError("It was not possible to provide the attribute \"%s\" in %r. "
37 "Check its availability in the current context." % (item, self))
38 # Get the dynamic attribute value, uncheck that it's being
39 # retrieved and return it
40 attr_value = self.provide_attr(item)
41 self.is_providing = False
42 return attr_value or default
To put simply, this class overrides the __getattr__
behavior and tries to retrieve the requested attributes using the functions it provides that the subclasses must implement.
From that base class, specific mixins can provide the attributes that can be retrieved in the available scope. Below are examples for the values mentioned before.
1class CleanedDataProviderMixin(AttrProviderMixin):
2 """ Provides values from `form.cleaned_data`. """
3
4 def can_provide_attr(self, attr):
5 return self.fields and attr in self.fields
6
7 def provide_attr(self, attr):
8 return self.cleaned_data.get(attr, None)
9
10class KwargsOrGetProviderMixin(AttrProviderMixin):
11 """ Provides values from the view's `kwargs` or GET parameters. """
12
13 def can_provide_attr(self, attr):
14 return \
15 self.kwargs and attr in self.kwargs or \
16 self.request.GET and attr in self.request.GET
17
18 def provide_attr(self, attr):
19 return \
20 self.kwargs.get(attr, None) or \
21 self.request.GET.get(attr, None)
22
23 def default_for_attr(self, attr):
24 if attr == 'param':
25 return 10
26 return None
To provide cleaned_data
, the field must exist in the form and then it's retrieved from cleaned_data
itself. As for kwargs
and GET
, the key must exist and then it's also simply retrieved from those attributes.
Finally, by adding those classes to the previous Form
and View
, retrieving the values can be done directly on the self
instance, as if they were class attributes.
1class MyForm(CleanedDataProviderMixin, Form):
2
3 # Your form goes here
4 pass
5
6class MyView(KwargsOrGetProviderMixin, FormView):
7
8 form_class = MyForm
9
10 def get(self, request, *args, **kwargs):
11 var_x = self.arg_1 * self.arg_2
12 self.some_func(var_x, self.param)
13
14 def form_valid(self, form):
15 var_1 = self.field_1 + self.field_2
16 var_2 = self.field_1 * self.field_3
17 self.other_func(var_1, var_2)
Ain't that better?
Of course it's necessary to check for conflicts with other attributes but most of the time I found that this improves my productivity and despite being unusual at first, it's very easy to understand and use this mechanism.
All the source code shown and explained here (mixins, functions, examples, samples, etc.) is available in the lemoncode
repository along with other helpful projects I’ve developed and wrote about. Feel free to check it out, use them in your own work and help me improve them with your feedback 🍋 🍋 🍋