Sponsor

Created With

linkDynamic Attribute Provider - Django

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 🍋 🍋 🍋

See it on GitHub
Dynamic Attribute Provider - Django

Home

Articleschevron_right