├── .gitignore ├── .travis.yml ├── AUTHORS.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── _templates │ ├── attributes.rst │ ├── details.rst │ ├── quick_start.rst │ ├── set_trace.rst │ └── variables.rst ├── conf.py └── index.rst ├── example ├── __init__.py ├── models.py ├── settings.py ├── templates │ ├── a.html │ └── home.html ├── urls.py ├── views.py └── wsgi.py ├── manage.py ├── requirements └── test.txt ├── runtests.py ├── setup.py ├── template_debug ├── __init__.py ├── models.py ├── templatetags │ ├── __init__.py │ └── debug_tags.py ├── tests │ ├── __init__.py │ ├── base.py │ ├── test_tags.py │ └── test_utils.py └── utils.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co~] 2 | *.orig 3 | ~.* 4 | dist/ 5 | *.egg-info/ 6 | .tox/ 7 | docs/_build 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | env: 5 | - TOXENV=py34-1.7 6 | - TOXENV=py34-1.6 7 | - TOXENV=py34-1.5 8 | - TOXENV=py33-1.7 9 | - TOXENV=py27-1.7 10 | - TOXENV=py33-1.6 11 | - TOXENV=py27-1.6 12 | - TOXENV=py33-1.5 13 | - TOXENV=py27-1.5 14 | - TOXENV=py27-1.4 15 | - TOXENV=py26-1.4 16 | - TOXENV=py27-1.3 17 | - TOXENV=py26-1.3 18 | install: 19 | - pip install -q --use-mirrors tox==1.4.2 20 | script: 21 | - tox 22 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Primary Author: 2 | 3 | Caleb Smith (calebsmith) 4 | 5 | Contributors: 6 | 7 | Stefan Klug (stefanklug) 8 | Mikhail Podgurskiy (kmmbvnr) 9 | Daniel Hahler (blueyed) 10 | Jeff Bradberry (jbradberry) 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Caleb Smith 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the Caktus Consulting Group, LLC nor the names of its 15 | contributors may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | include AUTHORS.txt 4 | recursive-include template_debug/templatetags * 5 | global-exclude /__pycache__ 6 | global-exclude *.pyc 7 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. readme_: 2 | 3 | django-template-debug 4 | ===================== 5 | 6 | A small collection of template tags for debugging and introspecting Django templates 7 | 8 | `Documentation `_ 9 | 10 | Requirements 11 | ************ 12 | None, but the latest ipdb is highly recommended. 13 | 14 | Installation 15 | ************ 16 | 17 | django-template-debug is available on pypi, so the easiest way to install it 18 | is using pip:: 19 | 20 | pip install django-template-debug 21 | 22 | Setup 23 | ***** 24 | Add 'template_debug' to the INSTALLED_APPS iterable in your settings file. For example:: 25 | 26 | INSTALLED_APPS = ( 27 | ... 28 | 'template_debug', 29 | ... 30 | ) 31 | 32 | Add ``TEMPLATE_DEBUG = True`` to your local or development settings if it is not already set. 33 | 34 | - Unless TEMPLATE_DEBUG is set to True, the django-template-debug templates will return an empty string without doing anything. This behavior prevents your application from calling set_trace() or print in a production environment if django-template-debug template tags are accidentally commited and deployed. 35 | 36 | Usage 37 | ***** 38 | 39 | Add {% load debug_tags %} in any Django template. 40 | 41 | The available tags to use are {% set_trace %} {% variables %} {% attributes varname %} and {% details varname %} 42 | 43 | See `Example Usage `_ docs for more details 44 | 45 | Developer Setup 46 | *************** 47 | 48 | Create a fresh virtualenv and install the test requirements:: 49 | 50 | mkvirtualenv template-debug 51 | pip install -r requirements/test.txt 52 | 53 | Use manage.py in the project directory along with the example.settings file 54 | for local testing. 55 | 56 | To run unittests using the virtualenv's Python and Django, use the `runtests` 57 | script. To test all supported versions of Python and Django, run the unittests 58 | using tox. 59 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pset.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pset.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Pset" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pset" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /docs/_templates/attributes.rst: -------------------------------------------------------------------------------- 1 | .. _attributes: 2 | 3 | ========== 4 | Attributes 5 | ========== 6 | 7 | Syntax: {% attributes %} 8 | 9 | Prints and returns the attributes of the variable that are available inside of Django templates. 10 | 11 | This tag will not show methods that are inaccessible within Django templates such as:: 12 | - Methods that are require arguments 13 | - Methods that have .alters_data = True set. (This is the default for save() and delete() methods of Django ORM instances) 14 | - Methods or attributes that are private (start with _) 15 | - Attributes that raise an expception when evaluated. 16 | 17 | Example: {% attributes request.user %} -> ['first_name', 'last_name', 'email', set_password', ...] -------------------------------------------------------------------------------- /docs/_templates/details.rst: -------------------------------------------------------------------------------- 1 | .. _details: 2 | 3 | ======= 4 | Details 5 | ======= 6 | 7 | Syntax: {% details %} 8 | 9 | Prints and returns a dictionary in the pattern {attribute: value} of the variable provided, for any attribute's value that can be obtained without raising an exception or making a method call. 10 | 11 | 12 | The exact behavior is as follows: 13 | - Only attributes or methods that are accessible inside of Django templates are shown. This functionality is shared with the attributes tag. See :ref:`attributes` for further details. 14 | - Any routine (function or method) returns with the value 'routine' rather than being called. This prevents the execution of user defined routines with side-effects that alter data or make network requests. 15 | - ORM managers return with 'ManyRelatedManager' or 'RelatedManager' to improve readability of the output when an ORM instance is given as the input. 16 | 17 | Example: {% details request.user %} -> { 'first_name': 'Joe', 'last_name': 'Sixpauk', 'set_password': 'routine', ...} 18 | -------------------------------------------------------------------------------- /docs/_templates/quick_start.rst: -------------------------------------------------------------------------------- 1 | .. _quick_start: 2 | 3 | ================= 4 | Quick Start Guide 5 | ================= 6 | 7 | After installing django-template-debug, simply load the debug tags in a template as follows:: 8 | 9 | {% load debug_tags %} 10 | 11 | Be sure to put your debug tags inside of a section that you are certain will be rendered. 12 | (e.g. make sure the tags are inside of a block tag that will be rendered and not inside of if tags) 13 | Alternatively, you might insert a debug tag inside of a conditional or for loop to determine if that branch is being executed in your template. 14 | 15 | 16 | Example Usage 17 | ************* 18 | 19 | The available tags are outlined briefly below and described more extensively in their linked documentation: 20 | 21 | - :ref:`set_trace` {% set_trace %}: 22 | - Drops the Django runserver into a set_trace debugger during template rendering 23 | - :ref:`attributes` {% attributes %}: 24 | - Given a variable name, prints and returns its attributes that are accessible within the current template 25 | - :ref:`variables` {% variables %}: 26 | - Prints and returns the list of variables available inside of the current context 27 | - :ref:`details` {% details %}: 28 | - Given a variable name, prints and returns a dictionary of the form {'attribute': value} for the attributes that are accessible within a Django template. 29 | -------------------------------------------------------------------------------- /docs/_templates/set_trace.rst: -------------------------------------------------------------------------------- 1 | .. _set_trace: 2 | 3 | ========= 4 | Set Trace 5 | ========= 6 | 7 | Syntax: {% set_trace %} 8 | 9 | Behavior: 10 | - Starts a set_trace while the template is being rendered. ipdb is used if available; otherwise pdb is used as a fallback. 11 | - The context is available inside of the set_trace as `context`. 12 | - The context variables are available in the local scope as the key provided in the context dictionary. (e.g. If a variable 'items' is in the context, it is available in the set trace as the variable 'items') 13 | 14 | 15 | Inside the Debugger 16 | ******************* 17 | 18 | Once inside the debugger, one can use details, attributes, variables, and render as functions as follows:: 19 | 20 | details(variable_name) 21 | attributes(variable_name) 22 | variables(context) 23 | render(string) 24 | 25 | The details, attributes, and variables functions work the same as their template tag counterparts. For each 26 | of these, refer to their corresponding pages for more details. 27 | 28 | The render function is a quick way to test out how a given template string would be rendered using the 29 | current context. For instance, typing `render('{{ now }}')` in a set trace will display the rendered string, 30 | pulling the variable `now` from the current context. 31 | 32 | Usages and Examples 33 | ******************* 34 | 35 | A common use case is to put a {% set_trace %} near the top of the template you want to debug somewhere that you can assure will be executed (e.g. not inside of a conditional or loop). However, other placements can prove fruitful. For example, another possiblity is to put the {% set_trace %} tag inside a for loop and inspect each element one at a time, and iterate using the continue command in the debugger, for example:: 36 | 37 | {% for item in items %} 38 | {% set_trace %} 39 | {% endfor %} 40 | 41 | Inside of the debugger: 42 | In: item 43 | Out: 44 | In: item.attribute 45 | Out: "Deck of Many Things" 46 | In: c 47 | In: item 48 | Out: 49 | In: item.attribute 50 | Out: "Portable Hole" 51 | In: render('{% if item.magical %}A magical item {% endif %}') 52 | Out: "A magical item" 53 | 54 | Using a similar technique, one might place {% set_trace %} inside of loops or conditional blocks to assure these blocks are or are not being executed given certain criteria. For example, if the given scenario causes the containing conditional to evaluate to true, the runserver will drop into a debugger, otherwise template rendering will continue as normal. 55 | -------------------------------------------------------------------------------- /docs/_templates/variables.rst: -------------------------------------------------------------------------------- 1 | .. _variables: 2 | 3 | ========= 4 | Variables 5 | ========= 6 | 7 | Syntax: {% variables %} 8 | 9 | Prints and returns the variables available in the current context. This will include the context provided by the view that called the current template as well as any context processors that are in use. 10 | 11 | Example: {% variables %} -> ['user', 'csrf_token', 'items'] 12 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Sator documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Jan 15 14:11:21 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | extensions = ['autodoc'] 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('../')) 22 | sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'django-template-debug' 47 | copyright = u'2012, Caleb Smith' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = '0.3.3' 55 | # The full version, including alpha/beta/rc tags. 56 | release = version 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'default' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'DjangoTemplateDebugdoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | # The paper size ('letter' or 'a4'). 176 | #latex_paper_size = 'letter' 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | #latex_font_size = '10pt' 180 | 181 | # Grouping the document tree into LaTeX files. List of tuples 182 | # (source start file, target name, title, author, documentclass [howto/manual]). 183 | latex_documents = [ 184 | ('index', 'DjangoTemplateDebug.tex', u'Django-Template-Debug Documentation', 185 | u'Caleb Smith', 'manual'), 186 | ] 187 | 188 | # The name of an image file (relative to this directory) to place at the top of 189 | # the title page. 190 | #latex_logo = None 191 | 192 | # For "manual" documents, if this is true, then toplevel headings are parts, 193 | # not chapters. 194 | #latex_use_parts = False 195 | 196 | # If true, show page references after internal links. 197 | #latex_show_pagerefs = False 198 | 199 | # If true, show URL addresses after external links. 200 | #latex_show_urls = False 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #latex_preamble = '' 204 | 205 | # Documents to append as an appendix to all manuals. 206 | #latex_appendices = [] 207 | 208 | # If false, no module index is generated. 209 | #latex_domain_indices = True 210 | 211 | 212 | # -- Options for manual page output -------------------------------------------- 213 | 214 | # One entry per manual page. List of tuples 215 | # (source start file, name, description, authors, manual section). 216 | man_pages = [ 217 | ('index', 'django-template-debug', u'Django-Template-Debug Documentation', 218 | [u'Caleb Smith'], 1) 219 | ] 220 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | ======== 4 | Contents 5 | ======== 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | _templates/quick_start 11 | _templates/set_trace 12 | _templates/attributes 13 | _templates/variables 14 | _templates/details 15 | 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calebsmith/django-template-debug/f3d52638da571164d63e5c8331d409b0743c628f/example/__init__.py -------------------------------------------------------------------------------- /example/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calebsmith/django-template-debug/f3d52638da571164d63e5c8331d409b0743c628f/example/models.py -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | import os 3 | 4 | PROJECT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) 5 | 6 | DEBUG = True 7 | TEMPLATE_DEBUG = DEBUG 8 | 9 | DATABASES = { 10 | 'default': { 11 | 'ENGINE': 'django.db.backends.sqlite3', 12 | 'NAME': ':memory:', 13 | } 14 | } 15 | 16 | TIME_ZONE = 'America/Chicago' 17 | LANGUAGE_CODE = 'en-us' 18 | SITE_ID = 1 19 | 20 | USE_I18N = True 21 | USE_L10N = True 22 | 23 | # Absolute filesystem path to the directory that will hold user-uploaded files. 24 | # Example: "/home/media/media.lawrence.com/media/" 25 | MEDIA_ROOT = '' 26 | 27 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 28 | # trailing slash. 29 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 30 | MEDIA_URL = '' 31 | 32 | # Absolute path to the directory static files should be collected to. 33 | # Don't put anything in this directory yourself; store your static files 34 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 35 | # Example: "/home/media/media.lawrence.com/static/" 36 | STATIC_ROOT = '' 37 | 38 | # URL prefix for static files. 39 | # Example: "http://media.lawrence.com/static/" 40 | STATIC_URL = '/static/' 41 | 42 | # Additional locations of static files 43 | STATICFILES_DIRS = ( 44 | os.path.join(PROJECT_PATH, 'static'), 45 | ) 46 | 47 | # List of finder classes that know how to find static files in 48 | # various locations. 49 | STATICFILES_FINDERS = ( 50 | 'django.contrib.staticfiles.finders.FileSystemFinder', 51 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 52 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 53 | ) 54 | 55 | # Make this unique, and don't share it with anybody. 56 | SECRET_KEY = 'super-secret' 57 | 58 | # List of callables that know how to import templates from various sources. 59 | TEMPLATE_LOADERS = ( 60 | 'django.template.loaders.filesystem.Loader', 61 | 'django.template.loaders.app_directories.Loader', 62 | # 'django.template.loaders.eggs.Loader', 63 | ) 64 | 65 | TEMPLATE_CONTEXT_PROCESSORS = ( 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | 'django.core.context_processors.debug', 69 | 'django.core.context_processors.media', 70 | 'django.core.context_processors.i18n', 71 | 'django.core.context_processors.static', 72 | 'django.core.context_processors.request', 73 | ) 74 | 75 | MIDDLEWARE_CLASSES = ( 76 | 'django.middleware.common.CommonMiddleware', 77 | 'django.contrib.sessions.middleware.SessionMiddleware', 78 | 'django.middleware.csrf.CsrfViewMiddleware', 79 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 80 | 'django.contrib.messages.middleware.MessageMiddleware', 81 | # Uncomment the next line for simple clickjacking protection: 82 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 83 | ) 84 | 85 | ROOT_URLCONF = 'example.urls' 86 | 87 | # Python dotted path to the WSGI application used by Django's runserver. 88 | WSGI_APPLICATION = 'example.wsgi.application' 89 | 90 | TEMPLATE_DIRS = ( 91 | os.path.join(PROJECT_PATH, 'templates'), 92 | ) 93 | 94 | INSTALLED_APPS = ( 95 | 'django.contrib.auth', 96 | 'django.contrib.contenttypes', 97 | 'django.contrib.sessions', 98 | 'django.contrib.sites', 99 | 'django.contrib.messages', 100 | 'django.contrib.staticfiles', 101 | 'template_debug', 102 | 'example' 103 | ) 104 | 105 | # A sample logging configuration. The only tangible logging 106 | # performed by this configuration is to send an email to 107 | # the site admins on every HTTP 500 error when DEBUG=False. 108 | # See http://docs.djangoproject.com/en/dev/topics/logging for 109 | # more details on how to customize your logging configuration. 110 | LOGGING = { 111 | 'version': 1, 112 | 'disable_existing_loggers': False, 113 | 'filters': {}, 114 | 'handlers': { 115 | 'mail_admins': { 116 | 'level': 'ERROR', 117 | 'class': 'django.utils.log.AdminEmailHandler' 118 | } 119 | }, 120 | 'loggers': { 121 | 'django.request': { 122 | 'handlers': ['mail_admins'], 123 | 'level': 'ERROR', 124 | 'propagate': True, 125 | }, 126 | } 127 | } -------------------------------------------------------------------------------- /example/templates/a.html: -------------------------------------------------------------------------------- 1 | {% extends 'home.html' %} 2 | {% load debug_tags %} 3 | {% block content %} 4 | {% endblock %} -------------------------------------------------------------------------------- /example/templates/home.html: -------------------------------------------------------------------------------- 1 | {% load debug_tags %} 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | {% block content %} 10 |

test

11 | {% endblock %} 12 |
13 | 14 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | try: 2 | from django.conf.urls import patterns, include, url 3 | except ImportError: 4 | from django.conf.urls.defaults import patterns, include, url 5 | from django.views.generic.base import TemplateView 6 | 7 | from example import views 8 | 9 | 10 | urlpatterns = patterns('', 11 | url(r'^$', TemplateView.as_view(template_name='home.html'), name='home'), 12 | url(r'^a/$', views.view_a, name='a') 13 | ) 14 | -------------------------------------------------------------------------------- /example/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def view_a(request): 5 | return render(request, 'a.html', {'a': 3}) 6 | -------------------------------------------------------------------------------- /example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | Django==1.7 2 | Sphinx==1.2.3 3 | tox==1.7.2 4 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | # Use the example.settings as the default settings module for testing 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | import django 9 | 10 | # For Django1.7, load everything 11 | if hasattr(django, 'setup'): 12 | django.setup() 13 | 14 | from django.conf import settings 15 | from django.test.utils import get_runner 16 | 17 | 18 | def runtests(): 19 | TestRunner = get_runner(settings) 20 | test_runner = TestRunner(verbosity=1, interactive=True, failfast=False) 21 | failures = test_runner.run_tests(['template_debug', ]) 22 | sys.exit(failures) 23 | 24 | 25 | if __name__ == '__main__': 26 | runtests() 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | import os 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def read_file(filename): 7 | """Read a file into a string""" 8 | path = os.path.abspath(os.path.dirname(__file__)) 9 | filepath = os.path.join(path, filename) 10 | try: 11 | return open(filepath).read() 12 | except IOError: 13 | return '' 14 | 15 | 16 | setup( 17 | name='django-template-debug', 18 | version=__import__('template_debug').__version__, 19 | author='Caleb Smith', 20 | author_email='caleb.smithnc@gmail.com', 21 | packages=find_packages(), 22 | include_package_data=True, 23 | url='https://github.com/calebsmith/django-template-debug', 24 | license='BSD', 25 | description=' '.join(__import__('template_debug').__doc__.splitlines()).strip(), 26 | classifiers=[ 27 | 'Topic :: Software Development :: Libraries :: Python Modules', 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 2.6', 33 | 'Programming Language :: Python :: 2.7', 34 | 'Programming Language :: Python :: 3', 35 | 'Programming Language :: Python :: 3.3', 36 | 'Programming Language :: Python :: 3.4', 37 | 'Topic :: Software Development :: Libraries :: Python Modules', 38 | 'Development Status :: 4 - Beta', 39 | 'Operating System :: OS Independent', 40 | 'Framework :: Django', 41 | 'Environment :: Web Environment' 42 | ], 43 | long_description=read_file('README.rst'), 44 | ) 45 | -------------------------------------------------------------------------------- /template_debug/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A small collection of template tags for debugging and introspecting templates 3 | """ 4 | 5 | 6 | __version__ = '0.3.6' 7 | -------------------------------------------------------------------------------- /template_debug/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calebsmith/django-template-debug/f3d52638da571164d63e5c8331d409b0743c628f/template_debug/models.py -------------------------------------------------------------------------------- /template_debug/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/calebsmith/django-template-debug/f3d52638da571164d63e5c8331d409b0743c628f/template_debug/templatetags/__init__.py -------------------------------------------------------------------------------- /template_debug/templatetags/debug_tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Template tags that aid in common debugging scenarios. 3 | """ 4 | 5 | from __future__ import unicode_literals 6 | 7 | from pprint import pprint 8 | 9 | from django.conf import settings 10 | from django import template 11 | import socket 12 | 13 | from template_debug.utils import get_variables, get_details, get_attributes 14 | 15 | register = template.Library() 16 | 17 | 18 | def require_template_debug(f): 19 | """Decorated function is a no-op if TEMPLATE_DEBUG is False""" 20 | def _(*args, **kwargs): 21 | TEMPLATE_DEBUG = getattr(settings, 'TEMPLATE_DEBUG', False) 22 | return f(*args, **kwargs) if TEMPLATE_DEBUG else '' 23 | return _ 24 | 25 | 26 | def _display_details(var_data): 27 | """ 28 | Given a dictionary of variable attribute data from get_details display the 29 | data in the terminal. 30 | """ 31 | meta_keys = (key for key in list(var_data.keys()) 32 | if key.startswith('META_')) 33 | for key in meta_keys: 34 | display_key = key[5:].capitalize() 35 | pprint('{0}: {1}'.format(display_key, var_data.pop(key))) 36 | pprint(var_data) 37 | 38 | 39 | @require_template_debug 40 | @register.simple_tag(takes_context=True) 41 | def variables(context): 42 | """ 43 | Given a context, return a flat list of variables available in the context. 44 | """ 45 | availables = get_variables(context) 46 | pprint(availables) 47 | return availables 48 | 49 | 50 | @require_template_debug 51 | @register.simple_tag 52 | def attributes(var): 53 | """ 54 | Given a variable in the template's context, print and return the list of 55 | attributes thare accessible inside of the template. For example, private 56 | attributes or callables that require arguments are excluded. 57 | """ 58 | attrs = get_attributes(var) 59 | pprint(attrs) 60 | return attrs 61 | 62 | 63 | @require_template_debug 64 | @register.simple_tag 65 | def details(var): 66 | """ 67 | Prints a dictionary showing the attributes of a variable, and if possible, 68 | their corresponding values. 69 | """ 70 | var_details = get_details(var) 71 | _display_details(var_details) 72 | return var_details 73 | 74 | 75 | @require_template_debug 76 | @register.simple_tag(takes_context=True) 77 | def set_trace(context): 78 | """ 79 | Start a pdb set_trace inside of the template with the context available as 80 | 'context'. Uses ipdb if available. 81 | """ 82 | try: 83 | import ipdb as pdb 84 | except ImportError: 85 | import pdb 86 | print("For best results, pip install ipdb.") 87 | print("Variables that are available in the current context:") 88 | render = lambda s: template.Template(s).render(context) 89 | availables = get_variables(context) 90 | pprint(availables) 91 | print('Type `availables` to show this list.') 92 | print('Type to access one.') 93 | print('Use render("template string") to test template rendering') 94 | # Cram context variables into the local scope 95 | for var in availables: 96 | locals()[var] = context[var] 97 | pdb.set_trace() 98 | return '' 99 | 100 | 101 | #cache a socket error when doing pydevd.settrace, to allow running without debugger 102 | pdevd_not_available = False 103 | 104 | 105 | @require_template_debug 106 | @register.simple_tag(takes_context=True) 107 | def pydevd(context): 108 | """ 109 | Start a pydev settrace 110 | """ 111 | global pdevd_not_available 112 | if pdevd_not_available: 113 | return '' 114 | try: 115 | import pydevd 116 | except ImportError: 117 | pdevd_not_available = True 118 | return '' 119 | render = lambda s: template.Template(s).render(context) 120 | availables = get_variables(context) 121 | for var in availables: 122 | locals()[var] = context[var] 123 | #catch the case where no client is listening 124 | try: 125 | pydevd.settrace() 126 | except socket.error: 127 | pdevd_not_available = True 128 | return '' 129 | -------------------------------------------------------------------------------- /template_debug/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_utils import * 2 | from .test_tags import * 3 | -------------------------------------------------------------------------------- /template_debug/tests/base.py: -------------------------------------------------------------------------------- 1 | "Test helper functions and base test cases." 2 | import random 3 | import string 4 | 5 | from django.template import RequestContext 6 | from django.contrib.auth.models import User 7 | from django.test import TestCase 8 | 9 | 10 | class TemplateDebugTestCase(TestCase): 11 | "Base test case with helpers for template debug tests." 12 | 13 | def get_random_string(self, length=10): 14 | return ''.join(random.choice(string.ascii_letters) 15 | for x in range(length)) 16 | 17 | def _get_context(self, request, dict_=None, processors=None): 18 | return RequestContext(request, dict_, processors=processors) 19 | 20 | def get_context(self, url='/'): 21 | context = self.client.get(url).context 22 | try: 23 | return context[0] 24 | except KeyError: 25 | return context 26 | 27 | def create_user(self, **kwargs): 28 | "Factory method for creating Users." 29 | defaults = { 30 | 'username': self.get_random_string(), 31 | 'email': '', 32 | 'password': self.get_random_string(), 33 | } 34 | defaults.update(kwargs) 35 | return User.objects.create_user(**defaults) 36 | -------------------------------------------------------------------------------- /template_debug/tests/test_tags.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | from template_debug.tests.base import TemplateDebugTestCase 4 | from template_debug.templatetags.debug_tags import require_template_debug 5 | 6 | 7 | try: 8 | from django.utils.six import PY3 9 | except ImportError: 10 | range = xrange 11 | PY3 = False 12 | 13 | 14 | @require_template_debug 15 | def test_func(): 16 | return 'test string' 17 | 18 | 19 | class RequireTemplateDebugTestCase(TemplateDebugTestCase): 20 | 21 | def test_empty_if_template_debug_false(self): 22 | settings.TEMPLATE_DEBUG = False 23 | self.assertEqual(test_func(), '') 24 | 25 | def test_unchanged_if_template_debug_true(self): 26 | settings.TEMPLATE_DEBUG = True 27 | self.assertEqual(test_func(), 'test string') 28 | -------------------------------------------------------------------------------- /template_debug/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from django.test.client import RequestFactory 2 | 3 | from template_debug.tests.base import TemplateDebugTestCase 4 | from template_debug.utils import (_flatten, get_variables, get_details, 5 | is_valid_in_template, get_attributes) 6 | 7 | 8 | try: 9 | from django.utils.six import PY3 10 | except ImportError: 11 | range = xrange 12 | PY3 = False 13 | 14 | 15 | class FlattenTestCase(TemplateDebugTestCase): 16 | """TestCase for _flatten""" 17 | 18 | def test_flattens_inner_list(self): 19 | "Assure arbitrarily nested lists are flattened" 20 | nested_list = [1, [2, [3, 4, [5], ], 6, 7], 8] 21 | self.assertEqual(list(_flatten(nested_list)), list(range(1, 9))) 22 | 23 | def test_flattens_tuples(self): 24 | "Assure nested tuples are also flattened" 25 | nested_tuples = (1, (2, 3, (4, ), 5), 6) 26 | self.assertEqual(list(_flatten(nested_tuples)), list(range(1, 7))) 27 | 28 | def test_flattens_sets(self): 29 | "Assure nested sets are flattened" 30 | nested_sets = set([1, frozenset([2, 3]), 4]) 31 | self.assertEqual(list(_flatten(nested_sets)), list(range(1, 5))) 32 | 33 | def test_flatten_nested_combinations(self): 34 | "Assure nested iterables are flattened" 35 | nested = [1, frozenset([2, 3]), (4, (5,), 6), [7], 8] 36 | self.assertEqual(list(_flatten(nested)), list(range(1, 9))) 37 | 38 | def test_flatten_generator(self): 39 | "Assure generators are flattened" 40 | gens = [1, list(range(2, 4)), (num for num in (4, list(range(5, 7))))] 41 | self.assertEqual(list(_flatten(gens)), list(range(1, 7))) 42 | 43 | def test_flatten_string_unchanged(self): 44 | "Assure strings are left intact" 45 | data = ['abc', ['abc', ['abc']], 'abc'] 46 | self.assertEqual(list(_flatten(data)), ['abc', 'abc', 'abc', 'abc']) 47 | 48 | 49 | def test_processor(request): 50 | return { 51 | 'custom_processor_var': 1, 52 | } 53 | 54 | 55 | class GetVariablesTestCase(TemplateDebugTestCase): 56 | """TestCase for get_variables""" 57 | 58 | def setUp(self): 59 | factory = RequestFactory() 60 | self.request = factory.get('/foo/') 61 | self.known_globals = ['request', 'user'] 62 | 63 | def test_global_context_processors(self): 64 | """ 65 | Assure get_variables contains known global context processors such as 66 | request and user 67 | """ 68 | variables = set(get_variables(self._get_context(self.request))) 69 | self.assertTrue(variables.issuperset(set(self.known_globals))) 70 | 71 | def test_returned_variable(self): 72 | """ 73 | Assure get_variables returns variables unique to the context 74 | """ 75 | variables = get_variables(self._get_context(self.request, {})) 76 | self.assertTrue('a' not in variables) 77 | variables = get_variables(self._get_context(self.request, {'a': 3})) 78 | self.assertTrue('a' in variables) 79 | 80 | def test_custom_processors(self): 81 | variables = get_variables(self._get_context( 82 | self.request, {}, processors=[]) 83 | ) 84 | self.assertTrue('custom_processor_var' not in variables) 85 | variables = get_variables(self._get_context( 86 | self.request, {}, processors=[test_processor]) 87 | ) 88 | self.assertTrue('custom_processor_var' in variables) 89 | 90 | 91 | class TestClass(object): 92 | 93 | def _private(self): 94 | return 'private' 95 | 96 | def takes_args(self, x): 97 | return x 98 | 99 | def alters_data(self): 100 | return 'data was changed' 101 | alters_data.alters_data = True 102 | 103 | def valid_method(self): 104 | return True 105 | 106 | def has_kwargs(self, foobars=None): 107 | return foobars 108 | 109 | 110 | class IsValidInTemplateTestCase(TemplateDebugTestCase): 111 | 112 | def setUp(self): 113 | request = RequestFactory().get('/foo/') 114 | test_object = TestClass() 115 | context = self._get_context(request, {'test_object': test_object}) 116 | self.test_object = context['test_object'] 117 | 118 | def test_private(self): 119 | is_valid = is_valid_in_template(self.test_object, '_private') 120 | self.assertEqual(is_valid, False, 121 | 'is_valid should be false for private methods' 122 | ) 123 | 124 | def test_takes_args(self): 125 | is_valid = is_valid_in_template(self.test_object, 'takes_args') 126 | self.assertEqual(is_valid, False, 127 | 'is_valid should be false methods that require arguments' 128 | ) 129 | 130 | def test_alters_data(self): 131 | is_valid = is_valid_in_template(self.test_object, 'alters_data') 132 | self.assertEqual(is_valid, False, 133 | 'is_valid should be false for the methods with .alters_data = True' 134 | ) 135 | 136 | def test_valid_method(self): 137 | is_valid = is_valid_in_template(self.test_object, 'valid_method') 138 | self.assertEqual(is_valid, True, 139 | 'is_valid should be true for methods that are accessible to templates' 140 | ) 141 | 142 | def test_has_kwargs(self): 143 | is_valid = is_valid_in_template(self.test_object, 'has_kwargs') 144 | self.assertEqual(is_valid, True, 145 | 'is_valid should be true for methods that take kwargs' 146 | ) 147 | 148 | 149 | class GetAttributesTestCase(TemplateDebugTestCase): 150 | 151 | def setUp(self): 152 | request = RequestFactory().get('/foo/') 153 | test_object = TestClass() 154 | context = self._get_context(request, {'test_object': test_object}) 155 | self.test_object = context['test_object'] 156 | 157 | def test_valid_list(self): 158 | valid_attributes = set(get_attributes(self.test_object)) 159 | self.assertEqual(set(['has_kwargs', 'valid_method']), valid_attributes, 160 | 'has_kwargs and valid_method are the only valid routines of TestObject' 161 | ) 162 | 163 | 164 | class GetDetailsTestCase(TemplateDebugTestCase): 165 | 166 | def setUp(self): 167 | self.user = self.create_user(username='test', password='test') 168 | self.client.login(username='test', password='test') 169 | 170 | def test_invalid_managers_hidden(self): 171 | """ 172 | Assure managers that aren't accessible from model instances are hidden 173 | """ 174 | user = self.get_context()['user'] 175 | user_details = get_details(user) 176 | invalid_managers = [] 177 | for attr in dir(user): 178 | try: 179 | getattr(user, attr) 180 | except: 181 | invalid_managers.append(attr) 182 | self.assertTrue(all([not manager in user_details.keys() 183 | for manager in invalid_managers])) 184 | 185 | def test_set_value_method(self): 186 | """Assure methods have their value set to 'method'""" 187 | user_details = get_details(self.get_context()['user']) 188 | self.assertEqual(user_details['get_full_name'], 'routine') 189 | 190 | def test_set_value_managers(self): 191 | user = self.get_context()['user'] 192 | user_details = get_details(user) 193 | managers = [] 194 | for key in user_details.keys(): 195 | value = getattr(self.user, key, None) 196 | kls = getattr(getattr(value, '__class__', ''), '__name__', '') 197 | if kls in ('ManyRelatedManager', 'RelatedManager', 'EmptyManager'): 198 | managers.append(key) 199 | for key, value in user_details.items(): 200 | if key in managers: 201 | self.assertTrue(value in 202 | ('ManyRelatedManager', 'RelatedManager', 'EmptyManager',) 203 | ) 204 | 205 | def test_module_and_class_added(self): 206 | user_details = get_details(self.get_context()['user']) 207 | self.assertEqual(user_details['META_module_name'], 208 | 'django.utils.functional') 209 | self.assertEqual(user_details['META_class_name'], 'User') 210 | 211 | def test_get_details_c_extensions(self): 212 | """ 213 | Ensures get_details works on objects with callables that are 214 | implemented in C extensions. inspect.getargspec fails with a TypeError 215 | for such callables, and get_details needs to handle this gracefully 216 | 217 | N.B. Only Python >=2.7 has bit_length C routine on Booleans so the test 218 | has to be skipped for Python2.6 219 | """ 220 | if hasattr(True, 'bit_length'): 221 | try: 222 | details = get_details(True) 223 | except TypeError: 224 | self.fail('Fails to handle C routines for call to inspect.argspec') 225 | self.assertEqual(details['bit_length'], 'routine') 226 | user_details = get_details(self.get_context()['user']) 227 | self.assertTrue(any(( 228 | user_details['META_module_name'], 'django.contrib.auth.models', 229 | user_details['META_module_name'], 'django.utils.functional' 230 | ))) 231 | self.assertTrue(any(( 232 | user_details['META_class_name'] == 'User', 233 | user_details['META_class_name'] == 'AnonymousUser' 234 | ))) 235 | -------------------------------------------------------------------------------- /template_debug/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from functools import partial 3 | from collections import Iterable 4 | from inspect import getargspec, isroutine 5 | 6 | 7 | try: 8 | from django.utils.six import PY3, string_types 9 | except ImportError: 10 | # Django < 1.5. No Python 3 support 11 | PY3 = False 12 | string_types = basestring 13 | 14 | 15 | def _flatten(iterable): 16 | """ 17 | Given an iterable with nested iterables, generate a flat iterable 18 | """ 19 | for i in iterable: 20 | if isinstance(i, Iterable) and not isinstance(i, string_types): 21 | for sub_i in _flatten(i): 22 | yield sub_i 23 | else: 24 | yield i 25 | 26 | 27 | def get_variables(context): 28 | """ 29 | Given a context, return a sorted list of variable names in the context 30 | """ 31 | return sorted(set(_flatten(context.dicts))) 32 | 33 | 34 | def get_details(var): 35 | """ 36 | Given a variable inside the context, obtain the attributes/callables, 37 | their values where possible, and the module name and class name if possible 38 | """ 39 | var_data = {} 40 | # Obtain module and class details if available and add them in 41 | module = getattr(var, '__module__', '') 42 | kls = getattr(getattr(var, '__class__', ''), '__name__', '') 43 | if module: 44 | var_data['META_module_name'] = module 45 | if kls: 46 | var_data['META_class_name'] = kls 47 | for attr in get_attributes(var): 48 | value = _get_detail_value(var, attr) 49 | if value is not None: 50 | var_data[attr] = value 51 | return var_data 52 | 53 | 54 | def _get_detail_value(var, attr): 55 | """ 56 | Given a variable and one of its attributes that are available inside of 57 | a template, return its 'method' if it is a callable, its class name if it 58 | is a model manager, otherwise return its value 59 | """ 60 | value = getattr(var, attr) 61 | # Rename common Django class names 62 | kls = getattr(getattr(value, '__class__', ''), '__name__', '') 63 | if kls in ('ManyRelatedManager', 'RelatedManager', 'EmptyManager'): 64 | return kls 65 | if callable(value): 66 | return 'routine' 67 | return value 68 | 69 | 70 | def get_attributes(var): 71 | """ 72 | Given a varaible, return the list of attributes that are available inside 73 | of a template 74 | """ 75 | is_valid = partial(is_valid_in_template, var) 76 | return list(filter(is_valid, dir(var))) 77 | 78 | 79 | def is_valid_in_template(var, attr): 80 | """ 81 | Given a variable and one of its attributes, determine if the attribute is 82 | accessible inside of a Django template and return True or False accordingly 83 | """ 84 | # Remove private variables or methods 85 | if attr.startswith('_'): 86 | return False 87 | # Remove any attributes that raise an acception when read 88 | try: 89 | value = getattr(var, attr) 90 | except: 91 | return False 92 | if isroutine(value): 93 | # Remove any routines that are flagged with 'alters_data' 94 | if getattr(value, 'alters_data', False): 95 | return False 96 | else: 97 | # Remove any routines that require arguments 98 | try: 99 | argspec = getargspec(value) 100 | num_args = len(argspec.args) if argspec.args else 0 101 | num_defaults = len(argspec.defaults) if argspec.defaults else 0 102 | if num_args - num_defaults > 1: 103 | return False 104 | except TypeError: 105 | # C extension callables are routines, but getargspec fails with 106 | # a TypeError when these are passed. 107 | pass 108 | return True 109 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | downloadcache = {toxworkdir}/_download/ 3 | envlist = py34-1.7,py34-1.6,py34-1.5,py33-1.7,py27-1.7,py33-1.6,py27-1.6,py33-1.5,py27-1.5,py27-1.4,py26-1.4,py27-1.3,py26-1.3 4 | 5 | [testenv] 6 | commands = {envpython} runtests.py 7 | 8 | [testenv:py34-1.7] 9 | basepython = python3.4 10 | deps = django>=1.7,<1.8 11 | 12 | [testenv:py34-1.6] 13 | basepython = python3.4 14 | deps = django>=1.6,<1.7 15 | 16 | [testenv:py34-1.5] 17 | basepython = python3.4 18 | deps = django>=1.5,<1.6 19 | 20 | [testenv:py33-1.7] 21 | basepython = python3.3 22 | deps = django>=1.7,<1.8 23 | 24 | [testenv:py33-1.6] 25 | basepython = python3.3 26 | deps = django>=1.6,<1.7 27 | 28 | [testenv:py33-1.5] 29 | basepython = python3.3 30 | deps = django>=1.5,<1.6 31 | 32 | [testenv:py27-1.7] 33 | basepython = python2.7 34 | deps = django>=1.7,<1.8 35 | 36 | [testenv:py27-1.6] 37 | basepython = python2.7 38 | deps = django>=1.6,<1.7 39 | 40 | [testenv:py27-1.5] 41 | basepython = python2.7 42 | deps = django>=1.5,<1.6 43 | 44 | [testenv:py27-1.4] 45 | basepython = python2.7 46 | deps = django>=1.4,<1.5 47 | 48 | [testenv:py27-1.3] 49 | basepython = python2.7 50 | deps = django>=1.3,<1.4 51 | 52 | [testenv:py26-1.4] 53 | basepython = python2.6 54 | deps = django>=1.4,<1.5 55 | 56 | [testenv:py26-1.3] 57 | basepython = python2.6 58 | deps = django>=1.3,<1.4 59 | 60 | #[testenv:docs] 61 | #basepython = python2.6 62 | #deps = Sphinx==1.1.3 63 | #commands = 64 | # {envbindir}/sphinx-build -a -n -b html -d docs/_build/doctrees docs #docs/_build/html 65 | --------------------------------------------------------------------------------