42 Commits

Author SHA1 Message Date
Matt Good
ee726eece2 Better setup.py description 2015-04-17 17:53:40 -07:00
Matt Good
1cd9ba69d1 Update CHANGES for 0.10 2015-04-17 17:49:30 -07:00
Matt Good
30a1cd399a Add docs note on Versions panel displaying other packages 2015-04-17 17:49:30 -07:00
Matt Good
0524e7f3e9 Update "Thanks" section of docs 2015-04-17 17:49:30 -07:00
Matt Good
14cd912df7 Fix style errors and add to automated checks
Adds a "stylecheck" to the Tox build config to automatically run flake8 checks.
Fixes existing flake8 issues.
2015-04-17 14:23:58 -07:00
Matt Good
f087311f7c Add missing sqlalchemy_error template
Missed adding this file in the last commit.
2015-04-17 14:19:41 -07:00
Matt Good
61d1fc2678 Display steps needed to display SQL queries
In the SQLAlchemy panel, detect if Flask-SQLAlchemy is not set up to record
queries for the current app, and display the necessary steps to set it up.

The following steps will be detected and displayed if needed:

* install Flask-SQLAlchemy package
* add the extension to this app
* set SQLALCHEMY_RECORD_QUERIES if DEBUG is False
2015-04-17 13:51:10 -07:00
Matt Good
6b2566c01f Enable toolbar on HTML5 pages without </body> tag
Includes the toolbar on HTML5 pages without an explicit </body> tag by checking
for the HTML5 `<!doctype html>`.

Fixes #79
2015-04-17 12:56:10 -07:00
Matt Good
31ea3bce41 Merge branch 'body-warning' 2015-04-16 14:54:32 -07:00
Matt Good
2d60ea6c8d Fix case-insensitive HTML insertion
Keeps the case-insensitive search for "</body>" and removes extra scan for the
tag by trying to insert the toolbar, and warning if unsuccessful.
2015-04-16 14:47:46 -07:00
Matt Good
56994a522d Document using init_app() for the factor pattern 2015-04-16 14:24:24 -07:00
Matt Good
aa90a9d052 Remove restyling table rows before sorting
It doesn't seem to be necessary to remove the "even / odd" styles before
sorting, since they are explicitly toggled to the right state after sorting. It
should be slightly more responsive to skip this step when sorting large tables.
2015-04-15 12:47:14 -07:00
Matt Good
32e0aa8ca3 Ensure profiler "Calls" are sorted numerically
JS Tablesorter's detection of the data type is occasionally confused when the
profiler calls show ratios like "94/57" for total calls to primitive calls.
This change enables parsing "data" attributes on the table headers to pass as
options to Tablesorter. This way we can explicitly specify to use the "digit"
sorter which works the way we want.

Fixes #62
2015-04-15 12:39:59 -07:00
Matt Good
e052b005ec Merge branch 'package-versions' 2015-04-14 19:21:45 -07:00
Matt Good
2f18ac90f9 Convert versions template to 2 spaces for indentation 2015-04-14 19:20:29 -07:00
Matt Good
612f93c129 Show relative paths for package paths
Since most of the paths are the same, only display the path relative to the
main Python library path.
2015-04-14 19:18:45 -07:00
Matt Good
c0826c9a31 Add detailed docs on the built-in panels 2015-04-14 17:22:07 -07:00
Matt Good
7e2de8f588 Merge pull request #48 from yoloseem/sqlparse
Suggestion: Use sqlparse.format() for better sql indentation
2015-04-14 15:44:00 -07:00
Matt Good
5aaa7a6634 Merge branch 'routes' 2015-04-14 15:28:16 -07:00
Matt Good
f7feecc751 Include the "static" route in the list
While this is enabled for most apps, it's optional and might still be useful to display.
2015-04-14 15:23:04 -07:00
Matt Good
cfe1624730 Simplify route methods display
Show just the names as text, without the Python list syntax formatting.
2015-04-14 14:58:31 -07:00
Matt Good
95d7eb977f Format route_list template with spaces 2015-04-14 14:57:47 -07:00
Matt Good
794380fc03 Merge pull request #81 from EricWorkman/master
Add tablesorter to slqlalchemy panel
2015-04-14 14:46:40 -07:00
Eric Workman
5a05620257 Add tablesorter to slqlalchemy panel 2015-04-10 15:44:38 -04:00
Matt Good
bebe884615 Update for 0.9.2 release 2014-12-05 15:43:46 -08:00
Matt Good
10c03880c7 Safely decode non-ascii SQL queries for display
SQL queries containing non-ascii byte strings would cause errors, both with and
without Pygments highlighting.

This updates the non-Pygments case to handle a simple decoding to ensure the
value is ascii-safe. It also removes passing an explicit "utf-8" encoding to
Pygments, since this causes errors when the bytes are not utf-8. When the
encoding is omitted, Pygments will default to "guess" the encoding by trying
utf-8 and falling back to latin-1.

Fixes #55
2014-12-04 14:06:48 -08:00
Matt Good
3fcdfc8f83 Use platform-sensitive filename normalization
The os.path.commonprefix() function only does basic string prefix checking, and
isn't aware of case-insensitive file names, or path separators. Instead, this
switches the filename formatting to use os.path.relpath() for normalizing paths
relative to sys.path.

Fixes #67
2014-12-02 18:15:59 -08:00
Matt Good
914553ddf5 Consolidate redundant code for SQL select/explain
The code for displaying the SQL select results and explain plan were nearly
identical, so this consolidates it to one function and template. It also fixes
correctly displaying the timing results as milliseconds.
2014-11-24 15:04:50 -08:00
Matt Good
b4fe0b954c Fix misspelled CSS classes 2014-11-24 14:46:41 -08:00
Matt Good
7557ee6794 Ensure SQL queries are HTML-escaped
The SQL queries were displayed with the `safe` filter which allowed properly
including the Pygments-highlighted HTML, but if Pygments wasn't installed this
allowed the raw SQL to be included without escaping. This change removes the
`safe` filter and instead wraps the Pygments HTML with the `Markup` class. This
allows proper auto-escaping in the template.

Fixes #70
2014-11-24 14:36:56 -08:00
Matt Good
382c5e7da9 Remove old commented code
Looks like some old code from the original Django toolbar was left in the
template.
2014-11-24 14:25:32 -08:00
Matt Good
6286dadc27 Version update for 0.9.1 release 2014-11-24 13:25:17 -08:00
=
81f8e34846 Removed printing of routes to console. 2014-03-23 15:14:28 +11:00
=
79717926a5 Added endpoints, HTTP methods, Is alias and Redirect to columns to route list. 2014-03-23 15:12:31 +11:00
Justin McKay
5084428c9d Added the Route List panel to show the routes that are available within Flask. 2014-03-22 23:57:48 +11:00
Matt Good
82295aa4aa printable filter should replace non-ascii bytes
In Python 2, when repr() returns bytes, replace any non-ascii bytes
with the unicode ? character to ensure that the result is printable.

Fixes #66
2014-02-02 14:46:41 -08:00
Matt Good
70488fc14a Fix Py3 support for bytes SQL queries
Fixes #64
2014-01-14 17:56:47 -08:00
Lucas Taylor
ccd8ba66b2 Display installed packages and versions in Versions panel (requires setup tools) 2013-03-27 17:28:51 -07:00
Lucas Taylor
093763909f Display installed packages and versions in Versions panel (requires Yolk) 2013-03-27 17:16:28 -07:00
Hyunjun Kim
bd2b65d068 Improve format_sql(): use sqlparse.format() 2013-03-11 13:33:41 +09:00
Rune Halvorsen
f6e37be73c Issue a warning, rather than an exception, when toolbar can't be inserted 2012-03-10 19:36:57 +01:00
Rune Halvorsen
71bd15a4d6 Added an exception when debug toolbar can't be inserted due to missing markup 2012-02-29 01:44:17 +01:00
40 changed files with 750 additions and 234 deletions

View File

@@ -1,8 +1,43 @@
Changes
=======
0.10.0 (2015-04-17)
-------------------
Enhancements:
- Added new "Routes" panel displaying URL routing rules (#69, thanks Justin McKay)
- "Versions" panel displays versions of all installed packages (#49, thanks Lucas Taylor)
- SQLAlchemy displays necessary setup steps to set up query recording
- Support reformatting SQL queries if ``sqlparse`` library is available (#48, thanks Hyunjun Kim)
- Enable sorting SQLAlchemy queries (#81, thanks Eric Workman)
- Support inserting toolbar on HTML5 pages without ``</body>`` tag
- Log a warning if unable to insert the toolbar (#20, thanks Rune Halvorsen)
Fixes:
- Ensure numeric sorting of profiler "Calls" column
0.9.2 (2014-12-05)
------------------
Fixes:
- HTML escape SQL queries when syntax highlighting is not available
- Use case-insensitive comparison to normalize filenames on Windows
- Fix exception when SQL query contained non-ASCII characters
0.9.1 (2014-11-24)
------------------
Fixes:
- Fix SQL queries with byte strings on Python 3
- Fix displaying values whose `repr()` contains unprintable characters
0.9.0 (2014-01-03)
--------
------------------
Enhancements:

BIN
docs/_static/example.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
docs/_static/screenshot-config-panel.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
docs/_static/screenshot-logger-panel.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
docs/_static/screenshot-time-panel.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -25,7 +25,10 @@ import sys, os
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.viewcode']
extensions = [
'sphinx.ext.viewcode',
'sphinx.ext.intersphinx',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -41,16 +44,16 @@ master_doc = 'index'
# General information about the project.
project = u'Flask-DebugToolbar'
copyright = u'2012, Matt Good'
copyright = u'2012-2015'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.9'
version = '0.10'
# The full version, including alpha/beta/rc tags.
release = '0.9.0'
release = '0.10.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@@ -86,18 +89,21 @@ pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
intersphinx_mapping = {
'flasksqlalchemy': ('http://flask-sqlalchemy.pocoo.org/2.0/', None)
}
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'flask_small'
html_theme = 'flask'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
'github_fork': 'mgood/flask-debugtoolbar',
'index_logo': None,
}

View File

@@ -1,21 +1,19 @@
.. Flask-DebugToolbar documentation master file, created by
sphinx-quickstart on Wed Feb 15 18:08:39 2012.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Flask-DebugToolbar
==================
This is a port of the excellent `django-debug-toolbar <https://github.com/django-debug-toolbar/django-debug-toolbar>`_
for Flask applications.
This extension adds a toolbar overlay to Flask applications containing useful information for debugging.
.. image:: _static/example.gif
Installation
------------
Installing is simple with pip::
Installing is simple with `pip`_::
$ pip install flask-debugtoolbar
.. _pip: https://pip.pypa.io/
Usage
-----
@@ -36,9 +34,16 @@ Setting up the debug toolbar is simple::
toolbar = DebugToolbarExtension(app)
The toolbar will automatically be injected into Jinja templates when debug mode is on.
In production, setting ``app.debug = False`` will disable the toolbar.
The toolbar will automatically be injected into HTML responses when debug mode
is on. In production, setting ``app.debug = False`` will disable the toolbar.
This extension also supports the Flask app factory pattern by separately
creating the toolbar and later initializing it for an app::
toolbar = DebugToolbarExtension()
# Then later on.
app = create_app('the-config.cfg')
toolbar.init_app(app)
Configuration
-------------
@@ -61,11 +66,26 @@ To change one of the config options, set it in the Flask app's config like::
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
Panels
------
.. toctree::
panels
Contributing
------------
Fork us `on GitHub <https://github.com/mgood/flask-debugtoolbar>`_
Thanks
------
This was based on the original `django-debug-toolbar`_. Thanks to `Michael van Tellingen`_ for the original development of this Flask extension, and to all the `individual contributors`_.
.. _django-debug-toolbar: https://github.com/django-debug-toolbar/django-debug-toolbar
.. _Michael van Tellingen: https://github.com/mvantellingen
.. _individual contributors: https://github.com/mgood/flask-debugtoolbar/graphs/contributors
Indices and tables
==================

106
docs/panels.rst Normal file
View File

@@ -0,0 +1,106 @@
Built-In Panels
===============
Versions
--------
flask_debugtoolbar.panels.versions.VersionDebugPanel
Shows the installed Flask version. The expanded view displays all installed packages and their versions as detected by ``setuptools``.
Time
----
flask_debugtoolbar.panels.timer.TimerDebugPanel
Shows the time taken to process the current request. The exapanded view includes the breakdown of CPU time, by user and system, wall clock time, and context switches.
.. image:: _static/screenshot-time-panel.png
HTTP Headers
------------
flask_debugtoolbar.panels.headers.HeaderDebugPanel
Displays the HTTP headers for the current request.
.. image:: _static/screenshot-headers-panel.png
Request Vars
------------
flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel
Displays details of the Flask request-related variables, including the view function parameters, cookies, session variables, and GET and POST variables.
.. image:: _static/screenshot-request-vars-panel.png
Config
------
flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel
Shows the contents of the Flask application's config dict ``app.config``.
.. image:: _static/screenshot-config-panel.png
Templates
---------
flask_debugtoolbar.panels.template.TemplateDebugPanel
Shows information about the templates rendered for this request, and the value of the template parameters provided.
.. image:: _static/screenshot-template-panel.png
SQLAlchemy
----------
flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel
Shows SQL queries run during the current request.
.. note:: This panel requires using the `Flask-SQLAlchemy`_ extension in order
to record the queries. See the Flask-SQLAlchemy
:ref:`flasksqlalchemy:quickstart` section to configure it.
For additional details on query recording see the
:py:func:`~flask.ext.sqlalchemy.get_debug_queries` documentation.
.. image:: _static/screenshot-sqlalchemy-panel.png
.. _Flask-SQLAlchemy: http://flask-sqlalchemy.pocoo.org/
Logging
-------
flask_debugtoolbar.panels.logger.LoggingPanel
Displays log messages recorded during the current request.
.. image:: _static/screenshot-logger-panel.png
Route List
----------
flask_debugtoolbar.panels.route_list.RouteListDebugPanel
Displays the Flask URL routing rules.
Profiler
--------
flask_debugtoolbar.panels.profiler.ProfilerDebugPanel
Reports profiling data for the current request. Due to the performance overhead, profiling is disabled by default. Click the checkmark to toggle profiling on or off. After enabling the profiler, refresh the page to re-run it with profiling.
.. image:: _static/screenshot-profiler-panel.png

View File

@@ -1,15 +1,14 @@
import os
import warnings
from flask import current_app, request, g
from flask import Blueprint, current_app, request, g, send_from_directory
from flask.globals import _request_ctx_stack
from flask import send_from_directory
from jinja2 import Environment, PackageLoader
from werkzeug.exceptions import HTTPException
from werkzeug.urls import url_quote_plus
from flask_debugtoolbar.toolbar import DebugToolbar
from flask_debugtoolbar.compat import iteritems
from flask import Blueprint
from flask_debugtoolbar.toolbar import DebugToolbar
from flask_debugtoolbar.utils import decode_text
module = Blueprint('debugtoolbar', __name__)
@@ -30,7 +29,7 @@ def replace_insensitive(string, target, replacement):
def _printable(value):
try:
return repr(value)
return decode_text(repr(value))
except Exception as e:
return '<repr(%s) raised %s: %s>' % (
object.__repr__(value), type(e).__name__, e)
@@ -98,6 +97,7 @@ class DebugToolbarExtension(object):
'flask_debugtoolbar.panels.template.TemplateDebugPanel',
'flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel',
'flask_debugtoolbar.panels.logger.LoggingPanel',
'flask_debugtoolbar.panels.route_list.RouteListDebugPanel',
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel',
),
}
@@ -147,7 +147,9 @@ class DebugToolbarExtension(object):
real_request = request._get_current_object()
self.debug_toolbars[real_request] = DebugToolbar(real_request, self.jinja_env)
self.debug_toolbars[real_request] = (
DebugToolbar(real_request, self.jinja_env))
for panel in self.debug_toolbars[real_request].panels:
panel.process_request(real_request)
@@ -156,11 +158,16 @@ class DebugToolbarExtension(object):
This is done by the dispatch_request method.
"""
real_request = request._get_current_object()
if real_request in self.debug_toolbars:
for panel in self.debug_toolbars[real_request].panels:
new_view = panel.process_view(real_request, view_func, view_kwargs)
if new_view:
view_func = new_view
try:
toolbar = self.debug_toolbars[real_request]
except KeyError:
return view_func
for panel in toolbar.panels:
new_view = panel.process_view(real_request, view_func, view_kwargs)
if new_view:
view_func = new_view
return view_func
def process_response(self, response):
@@ -187,20 +194,38 @@ class DebugToolbarExtension(object):
# If the http response code is 200 then we process to add the
# toolbar to the returned html response.
if (response.status_code == 200 and
if not (response.status_code == 200 and
response.is_sequence and
response.headers['content-type'].startswith('text/html')):
for panel in self.debug_toolbars[real_request].panels:
panel.process_response(real_request, response)
return response
if response.is_sequence:
response_html = response.data.decode(response.charset)
toolbar_html = self.debug_toolbars[real_request].render_toolbar()
response_html = response.data.decode(response.charset)
content = replace_insensitive(
response_html, '</body>', toolbar_html + '</body>')
content = content.encode(response.charset)
response.response = [content]
response.content_length = len(content)
no_case = response_html.lower()
body_end = no_case.rfind('</body>')
if body_end >= 0:
before = response_html[:body_end]
after = response_html[body_end:]
elif no_case.startswith('<!doctype html>'):
before = response_html
after = ''
else:
warnings.warn('Could not insert debug toolbar.'
' </body> tag not found in response.')
return response
toolbar = self.debug_toolbars[real_request]
for panel in toolbar.panels:
panel.process_response(real_request, response)
toolbar_html = toolbar.render_toolbar()
content = ''.join((before, toolbar_html, after))
content = content.encode(response.charset)
response.response = [content]
response.content_length = len(content)
return response

View File

@@ -6,7 +6,9 @@ class DebugPanel(object):
Base class for debug panels.
"""
# name = Base
has_content = False # If content returns something, set to true in subclass
# If content returns something, set to true in subclass
has_content = False
# If the client is able to activate/de-activate the panel
user_enable = False

View File

@@ -26,8 +26,8 @@ class ThreadTrackingHandler(logging.Handler):
def get_records(self, thread=None):
"""
Returns a list of records for the provided thread, of if none is provided,
returns a list for the current thread.
Returns a list of records for the provided thread, of if none is
provided, returns a list for the current thread.
"""
if thread is None:
thread = threading.currentThread()
@@ -57,8 +57,8 @@ def _init_once():
# Call werkzeug's internal logging to make sure it gets configured
# before we add our handler. Otherwise werkzeug will see our handler
# and not configure console logging for the request log.
# Werkzeug's default log level is INFO so this message probably won't be
# seen.
# Werkzeug's default log level is INFO so this message probably won't
# be seen.
try:
from werkzeug._internal import _log
except ImportError:
@@ -88,8 +88,8 @@ class LoggingPanel(DebugPanel):
def nav_subtitle(self):
# FIXME l10n: use ngettext
return "%s message%s" % \
(len(handler.get_records()), (len(handler.get_records()) == 1) and '' or 's')
num_records = len(handler.get_records())
return '%s message%s' % (num_records, '' if num_records == 1 else 's')
def title(self):
return _('Log Messages')

View File

@@ -1,10 +1,8 @@
import sys
try:
import cProfile as profile
except ImportError:
import profile
import functools
import os.path
import pstats
from flask import current_app

View File

@@ -35,10 +35,11 @@ class RequestVarsDebugPanel(DebugPanel):
def content(self):
context = self.context.copy()
context.update({
'get': [(k, self.request.args.getlist(k)) for k in self.request.args],
'post': [(k, self.request.form.getlist(k)) for k in self.request.form],
'cookies': [(k, self.request.cookies.get(k)) for k in self.request.cookies],
'view_func': ('%s.%s' % (self.view_func.__module__, self.view_func.__name__)
'get': self.request.args.lists(),
'post': self.request.form.lists(),
'cookies': self.request.cookies.items(),
'view_func': ('%s.%s' % (self.view_func.__module__,
self.view_func.__name__)
if self.view_func else '[unknown]'),
'view_args': self.view_args,
'view_kwargs': self.view_kwargs or {},

View File

@@ -0,0 +1,34 @@
from flask_debugtoolbar.panels import DebugPanel
from flask import current_app
_ = lambda x: x
class RouteListDebugPanel(DebugPanel):
"""
Panel that displays the URL routing rules.
"""
name = 'RouteList'
has_content = True
routes = []
def nav_title(self):
return _('Route List')
def title(self):
return _('Route List')
def url(self):
return ''
def nav_subtitle(self):
count = len(self.routes)
return '%s %s' % (count, 'route' if count == 1 else 'routes')
def process_request(self, request):
self.routes = list(current_app.url_map.iter_rules())
def content(self):
return self.render('panels/route_list.html', {
'routes': self.routes,
})

View File

@@ -21,8 +21,13 @@ def query_signer():
salt='fdt-sql-query')
def is_select(statement):
prefix = b'select' if isinstance(statement, bytes) else 'select'
return statement.lower().strip().startswith(prefix)
def dump_query(statement, params):
if not params or not statement.lower().strip().startswith('select'):
if not params or not is_select(statement):
return None
try:
@@ -38,12 +43,33 @@ def load_query(data):
abort(406)
# Make sure it is a select statement
if not statement.lower().strip().startswith('select'):
if not is_select(statement):
abort(406)
return statement, params
def extension_used():
return 'sqlalchemy' in current_app.extensions
def recording_enabled():
return (current_app.debug
or current_app.config.get('SQLALCHEMY_RECORD_QUERIES'))
def is_available():
return (json_available and sqlalchemy_available
and extension_used() and recording_enabled())
def get_queries():
if get_debug_queries:
return get_debug_queries()
else:
return []
class SQLAlchemyDebugPanel(DebugPanel):
"""
Panel that displays the time a response took in milliseconds.
@@ -52,9 +78,7 @@ class SQLAlchemyDebugPanel(DebugPanel):
@property
def has_content(self):
if not json_available or not sqlalchemy_available:
return True # will display an error message
return bool(get_debug_queries())
return bool(get_queries()) or not is_available()
def process_request(self, request):
pass
@@ -66,12 +90,12 @@ class SQLAlchemyDebugPanel(DebugPanel):
return _('SQLAlchemy')
def nav_subtitle(self):
if not json_available or not sqlalchemy_available:
count = len(get_queries())
if not count and not is_available():
return 'Unavailable'
if get_debug_queries:
count = len(get_debug_queries())
return "%d %s" % (count, "query" if count == 1 else "queries")
return '%d %s' % (count, 'query' if count == 1 else 'queries')
def title(self):
return _('SQLAlchemy queries')
@@ -80,16 +104,16 @@ class SQLAlchemyDebugPanel(DebugPanel):
return ''
def content(self):
if not json_available or not sqlalchemy_available:
msg = ['Missing required libraries:', '<ul>']
if not json_available:
msg.append('<li>simplejson</li>')
if not sqlalchemy_available:
msg.append('<li>Flask-SQLAlchemy</li>')
msg.append('</ul>')
return '\n'.join(msg)
queries = get_queries()
if not queries and not is_available():
return self.render('panels/sqlalchemy_error.html', {
'json_available': json_available,
'sqlalchemy_available': sqlalchemy_available,
'extension_used': extension_used(),
'recording_enabled': recording_enabled(),
})
queries = get_debug_queries()
data = []
for query in queries:
data.append({
@@ -105,10 +129,18 @@ class SQLAlchemyDebugPanel(DebugPanel):
@module.route('/sqlalchemy/sql_select', methods=['GET', 'POST'])
def sql_select():
@module.route('/sqlalchemy/sql_explain', methods=['GET', 'POST'],
defaults=dict(explain=True))
def sql_select(explain=False):
statement, params = load_query(request.args['query'])
engine = SQLAlchemy().get_engine(current_app)
if explain:
if engine.driver == 'pysqlite':
statement = 'EXPLAIN QUERY PLAN\n%s' % statement
else:
statement = 'EXPLAIN\n%s' % statement
result = engine.execute(statement, params)
return g.debug_toolbar.render('panels/sqlalchemy_select.html', {
'result': result.fetchall(),
@@ -116,22 +148,3 @@ def sql_select():
'sql': format_sql(statement, params),
'duration': float(request.args['duration']),
})
@module.route('/sqlalchemy/sql_explain', methods=['GET', 'POST'])
def sql_explain():
statement, params = load_query(request.args['query'])
engine = SQLAlchemy().get_engine(current_app)
if engine.driver == 'pysqlite':
query = 'EXPLAIN QUERY PLAN %s' % statement
else:
query = 'EXPLAIN %s' % statement
result = engine.execute(query, params)
return g.debug_toolbar.render('panels/sqlalchemy_explain.html', {
'result': result.fetchall(),
'headers': result.keys(),
'sql': format_sql(statement, params),
'duration': float(request.args['duration']),
})

View File

@@ -1,12 +1,10 @@
import collections
import json
import sys
import traceback
import uuid
from jinja2.exceptions import TemplateSyntaxError
from flask import (
template_rendered, request, g, render_template_string,
template_rendered, request, g,
Response, current_app, abort, url_for
)
from flask_debugtoolbar import module
@@ -95,7 +93,8 @@ def template_editor(key):
require_enabled()
# TODO set up special loader that caches templates it loads
# and can override template contents
templates = [t['template'] for t in TemplateDebugPanel.get_cache_for_key(key)]
templates = [t['template'] for t in
TemplateDebugPanel.get_cache_for_key(key)]
return g.debug_toolbar.render('panels/template_editor.html', {
'static_path': url_for('_debug_toolbar.static', filename=''),
'request': request,
@@ -131,6 +130,7 @@ def template_preview(key):
while tb.tb_next:
tb = tb.tb_next
msg = {'lineno': tb.tb_lineno, 'error': str(e)}
return Response(json.dumps(msg), status=400, mimetype='application/json')
return Response(json.dumps(msg), status=400,
mimetype='application/json')
finally:
del tb

View File

@@ -37,13 +37,14 @@ class TimerDebugPanel(DebugPanel):
def nav_subtitle(self):
# TODO l10n
if self.has_resource:
utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime
stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime
return 'CPU: %0.2fms (%0.2fms)' % ((utime + stime) * 1000.0, self.total_time)
else:
if not self.has_resource:
return 'TOTAL: %0.2fms' % (self.total_time)
utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime
stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime
return 'CPU: %0.2fms (%0.2fms)' % (
(utime + stime) * 1000.0, self.total_time)
def title(self):
return _('Resource Usage')
@@ -51,7 +52,8 @@ class TimerDebugPanel(DebugPanel):
return ''
def _elapsed_ru(self, name):
return getattr(self._end_rusage, name) - getattr(self._start_rusage, name)
return (getattr(self._end_rusage, name)
- getattr(self._start_rusage, name))
def content(self):
@@ -59,8 +61,8 @@ class TimerDebugPanel(DebugPanel):
stime = 1000 * self._elapsed_ru('ru_stime')
vcsw = self._elapsed_ru('ru_nvcsw')
ivcsw = self._elapsed_ru('ru_nivcsw')
minflt = self._elapsed_ru('ru_minflt')
majflt = self._elapsed_ru('ru_majflt')
# minflt = self._elapsed_ru('ru_minflt')
# majflt = self._elapsed_ru('ru_majflt')
# these are documented as not meaningful under Linux. If you're running BSD
# feel free to enable them, and add any others that I hadn't gotten to before
@@ -81,9 +83,9 @@ class TimerDebugPanel(DebugPanel):
(_('Total CPU time'), '%0.3f msec' % (utime + stime)),
(_('Elapsed time'), '%0.3f msec' % self.total_time),
(_('Context switches'), '%d voluntary, %d involuntary' % (vcsw, ivcsw)),
# ('Memory use', '%d max RSS, %d shared, %d unshared' % (rss, srss, urss + usrss)),
# ('Page faults', '%d no i/o, %d requiring i/o' % (minflt, majflt)),
# ('Disk operations', '%d in, %d out, %d swapout' % (blkin, blkout, swap)),
# ('Memory use', '%d max RSS, %d shared, %d unshared' % (rss, srss, urss + usrss)),
# ('Page faults', '%d no i/o, %d requiring i/o' % (minflt, majflt)),
# ('Disk operations', '%d in, %d out, %d swapout' % (blkin, blkout, swap)),
)
context = self.context.copy()

View File

@@ -1,15 +1,28 @@
import os
from distutils.sysconfig import get_python_lib
from flask import __version__ as flask_version
from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
def relpath(location, python_lib):
location = os.path.normpath(location)
relative = os.path.relpath(location, python_lib)
if relative == os.path.curdir:
return ''
elif relative.startswith(os.path.pardir):
return location
return relative
class VersionDebugPanel(DebugPanel):
"""
Panel that displays the Flask version.
"""
name = 'Version'
has_content = False
has_content = True
def nav_title(self):
return _('Versions')
@@ -24,4 +37,16 @@ class VersionDebugPanel(DebugPanel):
return _('Versions')
def content(self):
return None
try:
import pkg_resources
except ImportError:
packages = []
else:
packages = sorted(pkg_resources.working_set,
key=lambda p: p.project_name.lower())
return self.render('panels/versions.html', {
'packages': packages,
'python_lib': os.path.normpath(get_python_lib()),
'relpath': relpath,
})

View File

@@ -15,7 +15,7 @@
#flDebug { font-family: sans-serif; color: #000; background: #fff; }
#flDebug tbody, #flDebug code {
#flDebug tbody, #flDebug code, #flDebug pre {
font-family: Consolas, Monaco, "Bitstream Vera Sans Mono", "Lucida Console", monospace;
}
@@ -148,7 +148,7 @@
}
#flDebug code {
display:block;
display:inline;
white-space:pre;
overflow:auto;
}
@@ -219,6 +219,10 @@
margin-top:0.8em;
}
#flDebug h5 {
font-size: 14px;
}
#flDebug .panelContent table {
border:1px solid #ccc;
border-collapse:collapse;
@@ -246,6 +250,18 @@
padding-right:.5em;
}
#flDebug .panelContent ol li {
margin: 0 0 1em 2em;
}
#flDebug .panelContent pre {
border:1px solid #ccc;
background-color:#fff;
display:block;
margin:0.8em 0;
padding: 0.2em 0.5em;
}
#flDebug .flTemplateHideContextDiv {
background-color:#fff;
}
@@ -322,10 +338,6 @@
text-align: left;
}
#flDebug .flSqlExplain td {
white-space: pre;
}
#flDebug span.flDebugLineChart {
background-color:#777;
height:3px;

View File

@@ -106,16 +106,16 @@
} else {
fldt.show_toolbar(false);
}
$('#flDebug table.tablesorter')
.tablesorter()
.bind('sortStart', function() {
$(this).find('tbody tr')
.removeClass('flDebugEven')
.removeClass('flDebugOdd');
$('#flDebug table.tablesorter').each(function() {
var headers = {};
$(this).find('thead th').each(function(idx, elem) {
headers[idx] = $(elem).data();
});
$(this).tablesorter({headers: headers});
})
.bind('sortEnd', function() {
$(this).find('tbody tr').each(function(idx, elem) {
var even = idx % 2 == 0;
var even = idx % 2 === 0;
$(elem)
.toggleClass('flDebugEven', even)
.toggleClass('flDebugOdd', !even);

View File

@@ -1,12 +1,12 @@
<table id="debug_toolbar_profiler_table" class="tablesorter">
<thead>
<tr>
<th>Calls</th>
<th data-sorter="digit">Calls</th>
<th>Total Time (ms)</th>
<th>Per Call (ms)</th>
<th>Cumulative Time (ms)</th>
<th>Per Call (ms)</th>
<th>Function</th>
<th data-sorter="text">Function</th>
</tr>
</thead>
<tbody>
@@ -21,5 +21,4 @@
</tr>
{% endfor %}
</tbody>
</table>
</table>

View File

@@ -0,0 +1,28 @@
<table>
<thead>
<tr>
<th>URL route</th>
<th>Endpoint name</th>
<th>HTTP methods</th>
<th>Is alias</th>
<th>Redirect to</th>
</tr>
</thead>
<tbody>
{% if routes %}
{% for route in routes|sort(attribute='rule') %}
<tr>
<td>{{ route.rule }}</td>
<td>{{ route.endpoint }}</td>
<td>{{ route.methods|sort|join(', ') }}</td>
<td>{{ route.alias }}</td>
<td>{{ route.redirect_to }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td>No routes have been configured.</td>
</tr>
{% endif %}
</tbody>
</table>

View File

@@ -1,4 +1,4 @@
<table>
<table class="tablesorter">
<thead>
<tr>
<th>&nbsp;(ms)</th>
@@ -22,39 +22,7 @@
</td>
<td class="syntax">
<div class="flDebugSqlWrap">
<div class="flDebugSql">{{ query.sql|safe }}</div>
{#
{% if query.stacktrace %}
<div class="djSQLHideStacktraceDiv" style="display:none;">
<table>
<tr>
<th>{% trans "Line" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "File" %}</th>
</tr>
{% for file, line, method in query.stacktrace %}
<tr>
<td>{{ line }}</td>
<td><code>{{ method|escape }}</code></td>
<td><code>{{ file|escape }}</code></td>
</tr>
{% endfor %}
</table>
{% if query.template_info %}
<table>
{% for line in query.template_info.context %}
<tr>
<td>{{ line.num }}</td>
<td><code style="font-family: monospace;{% if line.highlight %}background-color: lightgrey{% endif %}">{{ line.content }}</code></td>
</tr>
{% endfor %}
</table>
<p><strong>{{ query.template_info.name|default:"(unknown)" }}</strong></p>
{% endif %}
</div>
{% endif %}
<span class="djDebugLineChart{% if query.is_slow %} djDebugLineChartWarning{% endif %}" style="width:{{ query.width_ratio }}%; left:{{ query.start_offset }}%;"></span>
#}
<div class="flDebugSql">{{ query.sql }}</div>
</div>
</td>
</tr>

View File

@@ -0,0 +1,49 @@
<h4>Queries Unavailable</h4>
<p>
The toolbar was unable to fetch the SQLAlchemy queries for this request.
To enable the SQLAlchemy query display, please:
</p>
<ol>
{% if not json_available or not sqlalchemy_available %}
<li>
<h5>Install required libraries:</h5>
<ul>
{% if not json_available %}
<li>simplejson</li>
{% endif %}
{% if not sqlalchemy_available %}
<li>Flask-SQLAlchemy</li>
{% endif %}
</ul>
</li>
{% endif %}
{% if not extension_used %}
<li>
<h5>Configure Flask-SQLAlchemy:</h5>
<p>
The Flask-SQLAlchemy extension needs to be configured for this application.
Please see the <a href="http://flask-sqlalchemy.pocoo.org/2.0/quickstart/">
Flask-SQLAlchemy documentation</a> for details.
</p>
</li>
{% endif %}
{% if not recording_enabled %}
<li>
<h5>Enable query recording:</h5>
<p>
Since this app is not currently running in <code>DEBUG</code> mode, Flask-SQLAlchemy will not record queries by default. To enable query recording in non-debug mode, set the following configuration value:
</p>
<pre>app.config['SQLALCHEMY_RECORD_QUERIES'] = True</pre>
<p>
See the
<a href="http://flask-sqlalchemy.pocoo.org/2.0/api/#flask.ext.sqlalchemy.get_debug_queries">
documention of Flask-SQLAlchemy's <code>get_debug_queries()</code></a>
for additional details.
</p>
</li>
{% endif %}
</ol>

View File

@@ -1,33 +0,0 @@
<div class="flDebugPanelTitle">
<a class="flDebugClose flDebugBack" href="">Back</a>
<h3>SQL Explained</h3>
</div>
<div class="flDebugPanelContent">
<div class="scroll">
<dl>
<dt>Executed SQL</dt>
<dd>{{ sql|safe }}</dd>
<dt>Time</dt>
<dd>{{ '%.4f'|format(duration) }} ms</dd>
</dl>
<table class="flSqlExplain">
<thead>
<tr>
{% for h in headers %}
<th>{{ h|upper }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in result %}
<tr class="{{ loop.cycle('fjDebugOdd', 'fjDebugEven') }}">
{% for column in row %}
<td>{{ column }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@@ -1,14 +1,14 @@
<div class="flDebugPanelTitle">
<a class="flDebugClose flDebugBack" href="">Back</a>
<h3>SQL Explained</h3>
<h3>SQL Details</h3>
</div>
<div class="flDebugPanelContent">
<div class="scroll">
<dl>
<dt>Executed SQL</dt>
<dd>{{ sql|safe }}</dd>
<dt>Time</dt>
<dd>{{ '%.4f'|format(duration) }} ms</dd>
<dd>{{ sql }}</dd>
<dt>Original query duration</dt>
<dd>{{ '%.4f'|format(duration * 1000) }} ms</dd>
</dl>
{% if result %}
<table class="flSqlSelect">

View File

@@ -0,0 +1,33 @@
<h4>Installed Packages</h4>
<p>
Installation paths relative to:
</p>
<pre>
{{ python_lib }}
</pre>
<table>
<thead>
<tr>
<th>Package</th>
<th>Version</th>
<th>Installed Path</th>
</tr>
</thead>
<tbody>
{% for package in packages %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ package.project_name }}</td>
<td>{{ package.version }}</td>
<td>{{ relpath(package.location, python_lib) }}</td>
</tr>
{% else %}
<tr>
<td>setuptools</td>
<td>NOT INSTALLED</td>
<td>Install setuptools to display installed packages and version information</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -30,7 +30,8 @@ class DebugToolbar(object):
activated = unquote(activated).split(';')
for panel_class in self._iter_panels(current_app):
panel_instance = panel_class(jinja_env=self.jinja_env, context=self.template_context)
panel_instance = panel_class(jinja_env=self.jinja_env,
context=self.template_context)
if panel_instance.dom_id() in activated:
panel_instance.is_active = True

View File

@@ -1,3 +1,4 @@
import itertools
import os.path
import sys
@@ -11,47 +12,75 @@ try:
except ImportError:
HAVE_PYGMENTS = False
try:
import sqlparse
HAVE_SQLPARSE = True
except ImportError:
HAVE_SQLPARSE = False
from flask import current_app
from flask import current_app, Markup
def format_fname(value):
# If the value is not an absolute path, the it is a builtin or
# a relative file (thus a project file).
# If the value has a builtin prefix, return it unchanged
if value.startswith(('{', '<')):
return value
value = os.path.normpath(value)
# If the file is absolute, try normalizing it relative to the project root
# to handle it as a project file
if os.path.isabs(value):
value = _shortest_relative_path(
value, [current_app.root_path], os.path)
# If the value is a relative path, it is a project file
if not os.path.isabs(value):
if value.startswith(('{', '<')):
return value
if value.startswith('.' + os.path.sep):
return value
return '.' + os.path.sep + value
return os.path.join('.', value)
# If the file is absolute and within the project root handle it as
# a project file
if value.startswith(current_app.root_path):
return "." + value[len(current_app.root_path):]
# Otherwise, normalize other paths relative to sys.path
return '<%s>' % _shortest_relative_path(value, sys.path, os.path)
# Loop through sys.path to find the longest match and return
# the relative path from there.
paths = sys.path
prefix = None
prefix_len = 0
for path in sys.path:
new_prefix = os.path.commonprefix([path, value])
if len(new_prefix) > prefix_len:
prefix = new_prefix
prefix_len = len(prefix)
if not prefix.endswith(os.path.sep):
prefix_len -= 1
path = value[prefix_len:]
return '<%s>' % path
def _shortest_relative_path(value, paths, path_module):
relpaths = _relative_paths(value, paths, path_module)
return min(itertools.chain(relpaths, [value]), key=len)
def _relative_paths(value, paths, path_module):
for path in paths:
try:
relval = path_module.relpath(value, path)
except ValueError:
# on Windows, relpath throws a ValueError for
# paths with different drives
continue
if not relval.startswith(path_module.pardir):
yield relval
def decode_text(value):
"""
Decode a text-like value for display.
Unicode values are returned unchanged. Byte strings will be decoded
with a text-safe replacement for unrecognized characters.
"""
if isinstance(value, bytes):
return value.decode('ascii', 'replace')
else:
return value
def format_sql(query, args):
if not HAVE_PYGMENTS:
return query
if HAVE_SQLPARSE:
query = sqlparse.format(query, reindent=True, keyword_case='upper')
return highlight(
if not HAVE_PYGMENTS:
return decode_text(query)
return Markup(highlight(
query,
SqlLexer(encoding='utf-8'),
HtmlFormatter(encoding='utf-8', noclasses=True, style=PYGMENT_STYLE))
SqlLexer(),
HtmlFormatter(noclasses=True, style=PYGMENT_STYLE)))

View File

@@ -14,14 +14,14 @@ except:
setup(
name='Flask-DebugToolbar',
version='0.9.0',
version='0.10.0',
url='http://flask-debugtoolbar.rtfd.org/',
license='BSD',
author='Michael van Tellingen',
author_email='michaelvantellingen@gmail.com',
maintainer='Matt Good',
maintainer_email='matt@matt-good.net',
description='A port of the Django debug toolbar to Flask',
description='A toolbar overlay for debugging Flask applications.',
long_description=README + '\n\n' + CHANGES,
zip_safe=False,
platforms='any',

View File

@@ -27,4 +27,4 @@ def index():
if __name__ == '__main__':
app.run()
app.run()

View File

@@ -1,3 +1,10 @@
import sys
import pytest
from flask_debugtoolbar import _printable
def load_app(name):
app = __import__(name).app
app.config['TESTING'] = True
@@ -9,3 +16,26 @@ def test_basic_app():
index = app.get('/')
assert index.status_code == 200
assert b'<div id="flDebug"' in index.data
@pytest.mark.skipif(sys.version_info >= (3,),
reason='test only applies to Python 2')
def test_printable_unicode():
class UnicodeRepr(object):
def __repr__(self):
return u'\uffff'
printable = _printable(UnicodeRepr())
assert "raised UnicodeEncodeError: 'ascii' codec" in printable
@pytest.mark.skipif(sys.version_info >= (3,),
reason='test only applies to Python 2')
def test_printable_non_ascii():
class NonAsciiRepr(object):
def __repr__(self):
return 'a\xffb'
printable = u'%s' % _printable(NonAsciiRepr())
# should replace \xff with the unicode ? character
assert printable == u'a\ufffdb'

123
test/test_utils.py Normal file
View File

@@ -0,0 +1,123 @@
import pytest
import posixpath
import ntpath
from flask import Markup
from flask_debugtoolbar.utils import (_relative_paths, _shortest_relative_path,
format_sql, decode_text, HAVE_PYGMENTS)
@pytest.mark.parametrize('value,paths,expected,path_module', [
# should yield relative path to the parent directory
('/foo/bar', ['/foo'], ['bar'], posixpath),
('c:\\foo\\bar', ['c:\\foo'], ['bar'], ntpath),
# should not yield result if no path is a parent directory
('/foo/bar', ['/baz'], [], posixpath),
('c:\\foo\\bar', ['c:\\baz'], [], ntpath),
# should only yield relative paths for parent directories
('/foo/bar', ['/foo', '/baz'], ['bar'], posixpath),
('c:\\foo\\bar', ['c:\\foo', 'c:\\baz'], ['bar'], ntpath),
# should yield all results when multiple parents match
('/foo/bar/baz', ['/foo', '/foo/bar'], ['bar/baz', 'baz'], posixpath),
('c:\\foo\\bar\\baz', ['c:\\foo', 'c:\\foo\\bar'],
['bar\\baz', 'baz'], ntpath),
# should ignore case differences on windows
('c:\\Foo\\bar', ['c:\\foo'], ['bar'], ntpath),
# should preserve original case
('/Foo/Bar', ['/Foo'], ['Bar'], posixpath),
('c:\\Foo\\Bar', ['c:\\foo'], ['Bar'], ntpath),
])
def test_relative_paths(value, paths, expected, path_module):
assert list(_relative_paths(value, paths, path_module)) == expected
@pytest.mark.parametrize('value,paths,expected,path_module', [
# should yield relative path to the parent directory
('/foo/bar', ['/foo'], 'bar', posixpath),
('c:\\foo\\bar', ['c:\\foo'], 'bar', ntpath),
# should return the original value if no path is a parent directory
('/foo/bar', ['/baz'], '/foo/bar', posixpath),
('c:\\foo\\bar', ['c:\\baz'], 'c:\\foo\\bar', ntpath),
# should yield shortest result when multiple parents match
('/foo/bar/baz', ['/foo', '/foo/bar'], 'baz', posixpath),
('c:\\foo\\bar\\baz', ['c:\\foo', 'c:\\foo\\bar'], 'baz', ntpath),
])
def test_shortest_relative_path(value, paths, expected, path_module):
assert _shortest_relative_path(value, paths, path_module) == expected
def test_decode_text_unicode():
value = u'\uffff'
decoded = decode_text(value)
assert decoded == value
def test_decode_text_ascii():
value = 'abc'
assert decode_text(value.encode('ascii')) == value
def test_decode_text_non_ascii():
value = b'abc \xff xyz'
assert isinstance(value, bytes)
decoded = decode_text(value)
assert not isinstance(decoded, bytes)
assert decoded.startswith('abc')
assert decoded.endswith('xyz')
@pytest.fixture()
def no_pygments(monkeypatch):
monkeypatch.setattr('flask_debugtoolbar.utils.HAVE_PYGMENTS', False)
def test_format_sql_no_pygments(no_pygments):
sql = 'select 1'
assert format_sql(sql, {}) == sql
def test_format_sql_no_pygments_non_ascii(no_pygments):
sql = b"select '\xff'"
formatted = format_sql(sql, {})
assert formatted.startswith(u"select '")
def test_format_sql_no_pygments_escape_html(no_pygments):
sql = 'select x < 1'
formatted = format_sql(sql, {})
assert not isinstance(formatted, Markup)
assert Markup('%s') % formatted == 'select x &lt; 1'
@pytest.mark.skipif(not HAVE_PYGMENTS,
reason='test requires the "Pygments" library')
def test_format_sql_pygments():
sql = 'select 1'
html = format_sql(sql, {})
assert isinstance(html, Markup)
assert html.startswith('<div')
assert 'select' in html
assert '1' in html
@pytest.mark.skipif(not HAVE_PYGMENTS,
reason='test requires the "Pygments" library')
def test_format_sql_pygments_non_ascii():
sql = b"select 'abc \xff xyz'"
html = format_sql(sql, {})
assert isinstance(html, Markup)
assert html.startswith('<div')
assert 'select' in html
assert 'abc' in html
assert 'xyz' in html

12
tox.ini
View File

@@ -1,9 +1,19 @@
[tox]
envlist = py26,py27,py33
envlist = py26,py27,py34,stylecheck
[testenv]
deps =
pytest
Flask-SQLAlchemy
Pygments
commands =
py.test
[testenv:stylecheck]
deps =
flake8
commands =
flake8 flask_debugtoolbar test
[flake8]
max-line-length = 100