Django:检测未使用的模板

3 投票
2 回答
1475 浏览
提问于 2025-04-17 12:45

有没有办法检测Django项目中未使用的模板呢?

在Django 1.3之前,可以通过一个简单的字符串匹配功能来实现,比如这个工具。但是自从1.3版本之后,Django引入了基于类的通用视图,如果你不自己指定,系统会自动生成一个template_name(比如DetailView)。

另外,如果你重写了第三方模块的模板,这些模板在你的视图中并没有直接使用。

也许可以通过遍历所有的URL定义,加载相应的视图,然后从中获取template_name来实现这个功能?

2 个回答

2

我很好奇是否可以通过猴子补丁(monkey patching)或装饰器来替代直接调用 get_template。我觉得是可以的,不过你得找到所有加载模板的函数(在我下面的例子中有两个)。

我使用了 wrapt,因为我发现这不仅仅是 loader.get_template 的问题,但它似乎能很好地解决这个问题。当然,最好把这个远离生产环境,安全第一,不过……

另外,我是通过单元测试和 nosetests 来驱动这个过程的,所以如果你的 Python 代码中使用模板的部分覆盖率足够高,你应该能找到大部分模板(假设我没有漏掉任何 get_template 相关的函数)。

settings.py

这里是用来补丁 get_template 及相关函数的“脑子”。

import wrapt
import django.template.loader
import django.template.engine

def wrapper(wrapped, instance, args, kwargs):

    #concatenate the args vector into a string.
    # print "\n\n\n\n%s\nI am a wrapper \nusage:%s\n%s\n\n\n\n\n" % ("*"*80, usage, "*"*80)
    try:
        return wrapped(*args, **kwargs)
    finally:
        usage = ",".join([unicode(arg) for arg in args if arg])
        track_usage(usage)

#you have to wrap whatever is loading templates...
#imported django module + class/method/function path of what needs to be
#wrapped within that module.  comment those 2 lines out and you are back to
#normal


wrapt.wrap_function_wrapper(django.template.loader, 'get_template', wrapper)
wrapt.wrap_function_wrapper(django.template.engine, 'Engine.find_template', wrapper)

想了解更多关于 wrapt 的信息,可以查看 安全应用猴子补丁。其实用起来比理解文档要简单多了,装饰器让我觉得脑袋疼。

另外,为了追踪哪些 Django 函数在实际加载模板,我故意在代码和模板中拼错了一些模板名称,运行单元测试后查看缺失模板的堆栈跟踪信息。

这是我写得相当糟糕的函数,它将数据添加到一个集合中并输出为 JSON 格式……

def track_usage(usage):
    fnp_usage = "./usage.json"

    try:
        with open(fnp_usage, "r") as fi:
            data = fi.read()
            #read the set of used templates from the json file
            j_data = json.loads(data)
            s_used_file = set(j_data.get("li_used"))

    except (IOError,),e:
            s_used_file = set()
            j_data = dict()

    s_used_file.add(usage)
    #convert the set back to a list for json compatibility
    j_data["li_used"] = list(s_used_file)

    with open(fnp_usage, "w") as fo:
        json.dump(j_data, fo)

输出结果(还有一个格式化的脚本):

import sys
import json
fnp_usage = sys.argv[1]


with open(fnp_usage, "r") as fi:
    data = fi.read()
    #read the set of used templates from the json file
    j_data = json.loads(data)
    li_used_file = j_data.get("li_used")
    li_used_file.sort()

    print "\n\nused templates:"
    for t in li_used_file:
        print(t)

通过包装上面这两个函数,似乎捕捉到了 extends、%includes 和直接的 get_templates,以及由基于类的视图使用的列表类型模板。它甚至捕捉到了我动态生成的模板,这些模板甚至不在文件系统上,而是通过自定义加载器加载的。

used templates:
bootstrap/display_form.html
bootstrap/errors.html
bootstrap/field.html
bootstrap/layout/baseinput.html
bootstrap/layout/checkboxselectmultiple.html
bootstrap/layout/field_errors.html
bootstrap/layout/field_errors_block.html
bootstrap/layout/help_text.html
bootstrap/layout/help_text_and_errors.html
bootstrap/layout/radioselect.html
bootstrap/whole_uni_form.html
django_tables2/table.html
dynamic_template:db:testdb:name:pssecurity/directive.PrimaryDetails.json
uni_form/layout/div.html
uni_form/layout/fieldset.html
websec/__base.html
websec/__full12.html
websec/__l_right_sidebar.html
websec/bootstrapped_home.html
websec/changedb.html
websec/login.html
websec/requirejs_config.html
websec/topnav.html
websec/user_msg.html
1

其实,想要准确找出哪些模板没有被用到是很难的,即使没有通用视图的情况下,因为你总是可以写出这样的代码:

get_template(any_code_you_like()).render(context)

所以在Django 1.3之前,你提到的django-unused-templates这个工具,只能在那些对模板使用有一定规范的项目中有效。(比如,像get_templaterender_to_response这样的函数,模板参数总是用字符串表示。)

而且,单单加载所有的视图也不够:因为一个视图在不同情况下可能会使用不同的模板:

def my_view(request):
    if request.user.is_authenticated():
        return render(request, 'template1.html')
    else:
        return render(request, 'template2.html')

当然,模板也可能根本不被视图使用,而是被系统的其他部分使用(比如,发送的电子邮件)。

撰写回答