Horrible Things I Regret Doing With Python Part One
Runtime Copy Paste With Global Variable Replacement.
"Could you make InlineAdmin in Django paginatable Joe?" Guido asked.I googled a while before regurgitating a Stackoverflow answer into Guido's lap.
"Oh cool, but we have multiple InlineAdmins, can you make it paginate for all of them?"
"Sure" I replied, "That'll just be a change to the admin class and some templates"
... Time passes
I had quickly changed the 'p' variable so it was dynamically determined by the model name assigned to the InlineAdmin class by adding a page_param property. The only problem was I that needed django's paginator_number template tag to take into account these new page variables as well.
from django.contrib.admin.views.main import ( ALL_VAR, ORDER_VAR, PAGE_VAR, SEARCH_VAR, ) ... @register.simple_tag def paginator_number(cl, i): """ Generates an individual page index link in a paginated list. """ if i == DOT: return '... ' elif i == cl.page_num: return format_html('<span class="this-page">{}</span> ', i + 1) else: return format_html('<a href="{}"{}>{}</a> ', cl.get_query_string({PAGE_VAR: i}), mark_safe(' class="end"' if i == cl.paginator.num_pages - 1 else ''), i + 1)
"Can you see what he's doing?" Tom called out, slightly exasperated as he looked at the hideous code I had concocted. I cackled slightly as the other devs gathered around my desk, the faint glow of monitor lighting their faces.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import copy import types from django.template import Library from django.contrib.admin.templatetags.admin_list import ( paginator_number, pagination ) register = Library() @register.simple_tag def parameterized_paginator_number(cl, i): '''This ridiculous code makes a copy of paginator_number function The PAGE_VAR variable is global in paginator number, this function copies paginator_number and replaces the global PAGE_VAR with a page_param that we can set. ''' # copy the globals of paginator_number func_globals_copy = copy.copy(paginator_number.__globals__) func_globals_copy['PAGE_VAR'] = cl.page_param paginator_number_func_copy = types.FunctionType( code=paginator_number.__code__, # pass our updated globals globals=func_globals_copy, name='parameterized_paginator_number', argdefs=paginator_number.__defaults__, closure=paginator_number.__closure__, ) return paginator_number_func_copy(cl, i) |
Here we use copy to make a copy of django's paginator_number's global (line 21). Then, in a horrible twist, we replace the PAGE_VAR in our new dict (line 22).
In line 24-31, we use the types library to create a new function. With exactly the same code as paginator_number, but crucially in line 27, we pass in our func_globals_copy, which contains the replaced PAGE_VAR global. Then we invoke our new paginator_number function copy and all is well.
Under the hood, each python function has a reference to a dict containing all the globals that the function uses, all we are doing is badly abusing this. #sorrynotsorry as they say.
"This is your fault django, you made me do this", I blurted out in some feeble attempt at an excuse following up with "I think it's less brittle than plain copy-pasting though".
"I actually agree." Tom said doing his best Patrick Stewart facepalm.