Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee726eece2 | ||
|
|
1cd9ba69d1 | ||
|
|
30a1cd399a | ||
|
|
0524e7f3e9 | ||
|
|
14cd912df7 | ||
|
|
f087311f7c | ||
|
|
61d1fc2678 | ||
|
|
6b2566c01f | ||
|
|
31ea3bce41 | ||
|
|
2d60ea6c8d | ||
|
|
56994a522d | ||
|
|
aa90a9d052 | ||
|
|
32e0aa8ca3 | ||
|
|
e052b005ec | ||
|
|
2f18ac90f9 | ||
|
|
612f93c129 | ||
|
|
c0826c9a31 | ||
|
|
7e2de8f588 | ||
|
|
5aaa7a6634 | ||
|
|
f7feecc751 | ||
|
|
cfe1624730 | ||
|
|
95d7eb977f | ||
|
|
794380fc03 | ||
|
|
5a05620257 | ||
|
|
bebe884615 | ||
|
|
10c03880c7 | ||
|
|
3fcdfc8f83 | ||
|
|
914553ddf5 | ||
|
|
b4fe0b954c | ||
|
|
7557ee6794 | ||
|
|
382c5e7da9 | ||
|
|
6286dadc27 | ||
|
|
81f8e34846 | ||
|
|
79717926a5 | ||
|
|
5084428c9d | ||
|
|
82295aa4aa | ||
|
|
70488fc14a | ||
|
|
ccd8ba66b2 | ||
|
|
093763909f | ||
|
|
bd2b65d068 | ||
|
|
f6e37be73c | ||
|
|
71bd15a4d6 |
37
CHANGES.rst
@@ -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
|
After Width: | Height: | Size: 156 KiB |
BIN
docs/_static/screenshot-config-panel.png
vendored
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
docs/_static/screenshot-headers-panel.png
vendored
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/_static/screenshot-logger-panel.png
vendored
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/_static/screenshot-profiler-panel.png
vendored
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/_static/screenshot-request-vars-panel.png
vendored
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/_static/screenshot-sqlalchemy-panel.png
vendored
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
docs/_static/screenshot-template-panel.png
vendored
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/_static/screenshot-time-panel.png
vendored
Normal file
|
After Width: | Height: | Size: 42 KiB |
18
docs/conf.py
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {},
|
||||
|
||||
34
flask_debugtoolbar/panels/route_list.py
Normal 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,
|
||||
})
|
||||
@@ -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']),
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
28
flask_debugtoolbar/templates/panels/route_list.html
Normal 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>
|
||||
@@ -1,4 +1,4 @@
|
||||
<table>
|
||||
<table class="tablesorter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th> (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>
|
||||
|
||||
49
flask_debugtoolbar/templates/panels/sqlalchemy_error.html
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
33
flask_debugtoolbar/templates/panels/versions.html
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
4
setup.py
@@ -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',
|
||||
|
||||
@@ -27,4 +27,4 @@ def index():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
app.run()
|
||||
|
||||
@@ -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
@@ -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 < 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
|
||||