mirror of
https://github.com/pallets-eco/flask-debugtoolbar.git
synced 2025-12-31 02:29:33 -06:00
Compare commits
151 Commits
0.12.0
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb30c1337c | ||
|
|
07f85152b5 | ||
|
|
cbac0064df | ||
|
|
58b4dd0290 | ||
|
|
e3ce6eb0a6 | ||
|
|
98c611ade9 | ||
|
|
95b02b5920 | ||
|
|
bd642464f2 | ||
|
|
5671a92e17 | ||
|
|
ad6323994f | ||
|
|
2361256107 | ||
|
|
e7b8136dea | ||
|
|
ccf5ae22c6 | ||
|
|
4e98b183f3 | ||
|
|
5b4f4a0fcd | ||
|
|
6f3eae808f | ||
|
|
ffcb3f58df | ||
|
|
b673c8d82a | ||
|
|
63308c7f49 | ||
|
|
b07b074223 | ||
|
|
e005409ac4 | ||
|
|
969bc454b6 | ||
|
|
e6ac868826 | ||
|
|
76a51dccf6 | ||
|
|
a1b9054479 | ||
|
|
2ea3823ae5 | ||
|
|
02d6beff23 | ||
|
|
2f07e43da0 | ||
|
|
654e80c494 | ||
|
|
877e69dc94 | ||
|
|
44ee4b5e3a | ||
|
|
05104beefc | ||
|
|
c3c3d5ec98 | ||
|
|
a2362ec4dd | ||
|
|
449405e3ba | ||
|
|
9b0b63465a | ||
|
|
6848e5440b | ||
|
|
c0539265ca | ||
|
|
b368ff9004 | ||
|
|
ad9f1c0783 | ||
|
|
314ef64e2e | ||
|
|
d4e1b1856b | ||
|
|
0fbc6210a8 | ||
|
|
7d3a6e3733 | ||
|
|
1f904cfa8c | ||
|
|
6174ae7539 | ||
|
|
193a3ed4f2 | ||
|
|
1c82c2861f | ||
|
|
e529ad95bc | ||
|
|
f4702f45fb | ||
|
|
a2a2b1382c | ||
|
|
a0ccd43e8f | ||
|
|
71b7f6f0ce | ||
|
|
cf69fb7f1e | ||
|
|
1d990e7f52 | ||
|
|
cdcf917044 | ||
|
|
691acc186b | ||
|
|
3573841522 | ||
|
|
879918429e | ||
|
|
4ccb3c9be6 | ||
|
|
128bd5af6f | ||
|
|
dfb101fb18 | ||
|
|
9c9f24a2f2 | ||
|
|
bfd4d8506f | ||
|
|
677bf794aa | ||
|
|
c218bb1084 | ||
|
|
ff3ed47d12 | ||
|
|
4225f7f4ad | ||
|
|
a63f64051b | ||
|
|
bf21647c4b | ||
|
|
818e5f2d62 | ||
|
|
c65c6b94e5 | ||
|
|
0ca1f6b241 | ||
|
|
48de4e4a3c | ||
|
|
b139c60a60 | ||
|
|
05f23436ac | ||
|
|
7012193220 | ||
|
|
7f3defff7c | ||
|
|
969cce532d | ||
|
|
c2804c4917 | ||
|
|
56beb35c36 | ||
|
|
98bf17aa58 | ||
|
|
716f05d953 | ||
|
|
fb28aa9d61 | ||
|
|
efe447fb5f | ||
|
|
9656a2cf33 | ||
|
|
9b63ad1837 | ||
|
|
765f22126e | ||
|
|
d4a8cc963e | ||
|
|
b7f5a725cd | ||
|
|
5bf5e093bb | ||
|
|
719fe02df5 | ||
|
|
f18bcc708a | ||
|
|
95c2b86bcd | ||
|
|
b03a2e6fb3 | ||
|
|
1c39a9ce47 | ||
|
|
8c8b2bb35c | ||
|
|
a2e773124f | ||
|
|
2f8ec9027b | ||
|
|
ab9a41df6a | ||
|
|
2b1e7d9907 | ||
|
|
d0360218fd | ||
|
|
e9fd3072a9 | ||
|
|
1aedfb0e2e | ||
|
|
e6ae9d0288 | ||
|
|
62ce443f8b | ||
|
|
2b8bf9cc44 | ||
|
|
42d859534a | ||
|
|
f959951185 | ||
|
|
e1c8704444 | ||
|
|
8a4cfa5e3c | ||
|
|
51d105afad | ||
|
|
15192f19e0 | ||
|
|
5712e57869 | ||
|
|
9571d06df5 | ||
|
|
3b25e114e9 | ||
|
|
f7ae3fd591 | ||
|
|
ce02d2da3c | ||
|
|
bd346a0fc1 | ||
|
|
ed8243e17e | ||
|
|
02c99a7b64 | ||
|
|
96514793e4 | ||
|
|
ec8cc3a0ba | ||
|
|
e3c8ab0ca2 | ||
|
|
fefb32b04d | ||
|
|
b5a7c032ab | ||
|
|
6af24f5f44 | ||
|
|
b4a197f87f | ||
|
|
15b6fee933 | ||
|
|
9e03576c94 | ||
|
|
bfa48c5a2c | ||
|
|
890fd9c7fb | ||
|
|
42a9651950 | ||
|
|
ff01910f6a | ||
|
|
9cdb04edcb | ||
|
|
f546d4633b | ||
|
|
db07028aa0 | ||
|
|
d44ab40729 | ||
|
|
0d409f54f5 | ||
|
|
708df1c07a | ||
|
|
10186a4202 | ||
|
|
3cd2dceace | ||
|
|
376c3deab3 | ||
|
|
45d3588bb6 | ||
|
|
15e8d77a49 | ||
|
|
4d84f262ae | ||
|
|
db64ce632c | ||
|
|
956d7501ec | ||
|
|
a758a9df7a | ||
|
|
5eea25882c | ||
|
|
e954cd9fae |
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
max_line_length = 88
|
||||
|
||||
[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}]
|
||||
indent_size = 2
|
||||
29
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug in Flask-DebugToolbar (not other projects which depend on Flask-DebugToolbar)
|
||||
---
|
||||
|
||||
<!--
|
||||
This issue tracker is a tool to address bugs in Flask-DebugToolbar itself.
|
||||
Please use GitHub Discussions or the Pallets Discord for questions about your
|
||||
own code.
|
||||
|
||||
Replace this comment with a clear outline of what the bug is.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Describe how to replicate the bug.
|
||||
|
||||
Include a minimal reproducible example that demonstrates the bug.
|
||||
Include the full traceback if there was an exception.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Describe the expected behavior that should have happened but didn't.
|
||||
-->
|
||||
|
||||
Environment:
|
||||
|
||||
- Python version:
|
||||
- Flask-DebugToolbar version:
|
||||
- Flask version:
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions on Discussions
|
||||
url: https://github.com/pallets-eco/flask-debugtoolbar/discussions/
|
||||
about: Ask questions about your own code on the Discussions tab.
|
||||
- name: Questions on Chat
|
||||
url: https://discord.gg/pallets
|
||||
about: Ask questions about your own code on our Discord chat.
|
||||
15
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
15
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature for Flask-DebugToolbar
|
||||
---
|
||||
|
||||
<!--
|
||||
Replace this comment with a description of what the feature should do.
|
||||
Include details such as links to relevant specs or previous discussions.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Replace this comment with an example of the problem which this feature
|
||||
would resolve. Is this problem solvable without changes to Flask-DebugToolbar,
|
||||
such as by subclassing or using an extension?
|
||||
-->
|
||||
18
.github/dependabot.yml
vendored
Normal file
18
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- '*'
|
||||
- package-ecosystem: pip
|
||||
directory: /requirements/
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
python-requirements:
|
||||
patterns:
|
||||
- '*'
|
||||
25
.github/pull_request_template.md
vendored
Normal file
25
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
Before opening a PR, open a ticket describing the issue or feature the
|
||||
PR will address. An issue is not required for fixing typos in
|
||||
documentation, or other simple non-code changes.
|
||||
|
||||
Replace this comment with a description of the change. Describe how it
|
||||
addresses the linked ticket.
|
||||
-->
|
||||
|
||||
<!--
|
||||
Link to relevant issues or previous PRs, one per line. Use "fixes" to
|
||||
automatically close an issue.
|
||||
|
||||
fixes #<issue number>
|
||||
-->
|
||||
|
||||
<!--
|
||||
Ensure each step in the contributing guide is complete, especially the following:
|
||||
|
||||
- Add tests that demonstrate the correct behavior of the change. Tests
|
||||
should fail without the change.
|
||||
- Add or update relevant docs, in the docs folder and in code.
|
||||
- Add an entry in CHANGES.rst summarizing the change and linking to the issue.
|
||||
- Add `.. versionchanged::` entries in any relevant code docs.
|
||||
-->
|
||||
23
.github/workflows/lock.yaml
vendored
Normal file
23
.github/workflows/lock.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Lock inactive closed issues
|
||||
# Lock closed issues that have not received any further activity for two weeks.
|
||||
# This does not close open issues, only humans may do that. It is easier to
|
||||
# respond to new issues with fresh examples rather than continuing discussions
|
||||
# on old issues.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
concurrency:
|
||||
group: lock
|
||||
jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
with:
|
||||
issue-inactive-days: 14
|
||||
pr-inactive-days: 14
|
||||
discussion-inactive-days: 14
|
||||
73
.github/workflows/publish.yaml
vendored
Normal file
73
.github/workflows/publish.yaml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Publish
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
hash: ${{ steps.hash.outputs.hash }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*/*.txt
|
||||
- run: pip install -r requirements/build.txt
|
||||
# Use the commit date instead of the current date during the build.
|
||||
- run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
|
||||
- run: python -m build
|
||||
# Generate hashes used for provenance.
|
||||
- name: generate hash
|
||||
id: hash
|
||||
run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT
|
||||
- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
with:
|
||||
path: ./dist
|
||||
provenance:
|
||||
needs: [build]
|
||||
permissions:
|
||||
actions: read
|
||||
id-token: write
|
||||
contents: write
|
||||
# Can't pin with hash due to how this workflow works.
|
||||
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
|
||||
with:
|
||||
base64-subjects: ${{ needs.build.outputs.hash }}
|
||||
create-release:
|
||||
# Upload the sdist, wheels, and provenance to a GitHub release. They remain
|
||||
# available as build artifacts for a while as well.
|
||||
needs: [provenance]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
- name: create release
|
||||
run: >
|
||||
gh release create --draft --repo ${{ github.repository }}
|
||||
${{ github.ref_name }}
|
||||
*.intoto.jsonl/* artifact/*
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
publish-pypi:
|
||||
needs: [provenance]
|
||||
# Wait for approval before attempting to upload to PyPI. This allows reviewing the
|
||||
# files in the draft release.
|
||||
environment:
|
||||
name: publish
|
||||
url: https://pypi.org/project/Flask-DebugToolbar/${{ github.ref_name }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
|
||||
- uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
packages-dir: artifact/
|
||||
- uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2
|
||||
with:
|
||||
packages-dir: artifact/
|
||||
55
.github/workflows/tests.yaml
vendored
Normal file
55
.github/workflows/tests.yaml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- '*.x'
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.rst'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '*.md'
|
||||
- '*.rst'
|
||||
jobs:
|
||||
tests:
|
||||
name: ${{ matrix.name || matrix.python }}
|
||||
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- {python: '3.12'}
|
||||
- {python: '3.11'}
|
||||
- {python: '3.10'}
|
||||
- {python: '3.9'}
|
||||
- {python: '3.8'}
|
||||
- {name: Minimal, python: '3.12', tox: minimal}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
allow-prereleases: true
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*/*.txt
|
||||
- run: pip install tox
|
||||
- run: tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }}
|
||||
typing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
|
||||
with:
|
||||
python-version: '3.x'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*/*.txt
|
||||
- name: cache mypy
|
||||
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
|
||||
with:
|
||||
path: ./.mypy_cache
|
||||
key: mypy|${{ hashFiles('pyproject.toml') }}
|
||||
- run: pip install tox
|
||||
- run: tox run -e typing
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,6 +1,10 @@
|
||||
*.egg-info
|
||||
*.pyc
|
||||
build/
|
||||
.idea/
|
||||
.vscode/
|
||||
.venv*/
|
||||
venv*/
|
||||
__pycache__/
|
||||
dist/
|
||||
docs/_build
|
||||
.tox
|
||||
.coverage*
|
||||
htmlcov/
|
||||
.tox/
|
||||
docs/_build/
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
||||
[submodule "docs/_themes"]
|
||||
path = docs/_themes
|
||||
url = git://github.com/mitsuhiko/flask-sphinx-themes.git
|
||||
21
.pre-commit-config.yaml
Normal file
21
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
ci:
|
||||
autoupdate_schedule: monthly
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.3
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
exclude: "(codemirror|jquery)"
|
||||
- id: debug-statements
|
||||
exclude: "(codemirror|jquery)"
|
||||
- id: fix-byte-order-marker
|
||||
exclude: "(codemirror|jquery)"
|
||||
- id: trailing-whitespace
|
||||
exclude: "(codemirror|jquery)"
|
||||
- id: end-of-file-fixer
|
||||
exclude: "(codemirror|jquery)"
|
||||
13
.readthedocs.yaml
Normal file
13
.readthedocs.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: 2
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: '3.12'
|
||||
python:
|
||||
install:
|
||||
- requirements: requirements/docs.txt
|
||||
- method: pip
|
||||
path: .
|
||||
sphinx:
|
||||
builder: dirhtml
|
||||
fail_on_warning: true
|
||||
@@ -1,9 +0,0 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python: "3.8"
|
||||
env:
|
||||
- TOXENV=py27
|
||||
- TOXENV=py38
|
||||
install:
|
||||
- pip install tox
|
||||
script: tox
|
||||
189
CHANGES.rst
189
CHANGES.rst
@@ -1,189 +0,0 @@
|
||||
Changes
|
||||
=======
|
||||
|
||||
0.12.0 (2022-03-28)
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
- Add flask.g section to show g object content. by @Yaser-Amiri in https://github.com/flask-debugtoolbar/flask-debugtoolbar/pull/118
|
||||
- Support gzip response by @zaw007 in https://github.com/flask-debugtoolbar/flask-debugtoolbar/pull/154
|
||||
- Update PyPI metadata files: add `setup.cfg` etc by @jeffwidman in https://github.com/flask-debugtoolbar/flask-debugtoolbar/pull/164
|
||||
|
||||
Fixes:
|
||||
- Remove deprecated Jinja with_ extension for Jinja 3.0 (related to Flask 2.0) by @nickjj in https://github.com/flask-debugtoolbar/flask-debugtoolbar/pull/157
|
||||
- Fix SQLAlchemy SELECT/EXPLAIN to use url_for to respect app prefixes.… by @mattaw in https://github.com/flask-debugtoolbar/flask-debugtoolbar/pull/143
|
||||
- Setup DB properly by @jeffwidman in https://github.com/flask-debugtoolbar/flask-debugtoolbar/pull/148
|
||||
- prefixed css classes, fixes #152 by @jnnkB in https://github.com/flask-debugtoolbar/flask-debugtoolbar/pull/153
|
||||
|
||||
|
||||
0.11.0 (2020-02-18)
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Switch to Flask's native CLI, dropping flask_script in the process (b92391d, thanks @jeffwidman)
|
||||
- Do not show DebugToolbar routes in the route map (#86, thanks @floqqi)
|
||||
- Document Pygments for SQL highlighting (#127, thanks @pgiraud)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Remove deprecated flask.json_available (#119, thanks @davidism)
|
||||
- Remove deprecated request.is_xhr (7ce099c, thanks @jeffwidman)
|
||||
- Explicitly disable `SQLALCHEMY_TRACK_MODIFICATIONS` (9c7db48, thanks @jeffwidman)
|
||||
- Fix typo (#142, thanks @timgates42)
|
||||
|
||||
|
||||
0.10.1 (2017-02-12)
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
- Add support for Python wheels
|
||||
|
||||
Fixes:
|
||||
|
||||
- Switch imports from deprecated flask.ext.* to flask_* syntax (#94, thanks
|
||||
Michael Lenzen & #97 thanks Iuri de Silvio)
|
||||
|
||||
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:
|
||||
|
||||
- Python 3 compatibility (#54, thanks justinmayer and jmagnusson)
|
||||
- Support .init_app() (#38)
|
||||
- New "Config" panel displaying Flask config values (#51, thanks Alexey Diyan)
|
||||
- Better PEP8-style formatting (#63, thanks Ivan Ivaschenko)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fix template editor with non-ASCII templates (#46)
|
||||
|
||||
|
||||
0.8 (2013-02-21)
|
||||
----------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Use `itsdangerous <http://pythonhosted.org/itsdangerous/>`_ to sign SQL queries
|
||||
- Expose the jQuery object as ``fldt.$`` so extensions can use the toolbar's
|
||||
copy of jQuery (#42)
|
||||
|
||||
Fixes:
|
||||
|
||||
- Don't intercept redirects on XHR requests (#41)
|
||||
- Fix SQL query time display as milliseconds (#36)
|
||||
- Fix ``functools.partial`` error (#35)
|
||||
- Fix werkzeug request logging with logging panel (#33)
|
||||
- Fix SQL panel unicode encoding error (#31)
|
||||
|
||||
|
||||
0.7.1 (2012-05-18)
|
||||
------------------
|
||||
|
||||
Fixes:
|
||||
|
||||
- loading template editor in-place over current page
|
||||
|
||||
|
||||
0.7 (2012-05-18)
|
||||
----------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Add an in-browser template editor to the template panel
|
||||
- ``DEBUG_TB_PROFILER_ENABLED`` config option to enable the profiler on all
|
||||
requests (normally it is user-enabled by clicking the checkmark)
|
||||
|
||||
|
||||
0.6.3.1 (2012-04-16)
|
||||
--------------------
|
||||
|
||||
New release to add missing changelog for 0.6.3
|
||||
|
||||
|
||||
0.6.3 (2012-04-16)
|
||||
------------------
|
||||
Fixes:
|
||||
|
||||
- Compatibility with Flask-SQLAlchemy 0.16 package name
|
||||
|
||||
|
||||
0.6.2 (2012-02-18)
|
||||
------------------
|
||||
|
||||
Fixes:
|
||||
|
||||
- Installation issue on Windows with trailing slashes in MANIFEST.in
|
||||
|
||||
- JavaScript error when using conditional comments for ``<html>`` tag
|
||||
(like in HTML5 Boilerplate)
|
||||
|
||||
|
||||
0.6.1 (2012-02-15)
|
||||
------------------
|
||||
|
||||
Fixes:
|
||||
|
||||
- Memory leak when toolbar was enabled
|
||||
|
||||
- UnicodeDecodeError when request data contained binary data (e.g. session values)
|
||||
|
||||
|
||||
Enhancements:
|
||||
|
||||
- ``DEBUG_TB_ENABLED`` config setting to explicitly enable or disable the toolbar
|
||||
|
||||
- ``DEBUG_TB_HOSTS`` config setting to enable toolbar only for specific remote hosts
|
||||
|
||||
- New logo for Flask instead of Django
|
||||
|
||||
- Monospaced font on table data
|
||||
|
||||
Thanks to kennethreitz and joeshaw for their contributions.
|
||||
|
||||
|
||||
0.6 (2012-01-04)
|
||||
----------------
|
||||
|
||||
Flask 0.8 or higher is required
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Flask 0.8 compatibility
|
||||
|
||||
Thanks to mvantellingen
|
||||
@@ -1,24 +1,21 @@
|
||||
Copyright (c) Rob Hudson and individual contributors.
|
||||
All rights reserved.
|
||||
Copyright 2011 Pallets Community Ecosystem
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of Django nor the names of its contributors may be used
|
||||
to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
@@ -1,3 +0,0 @@
|
||||
include LICENSE
|
||||
recursive-include flask_debugtoolbar/templates *.html
|
||||
recursive-include flask_debugtoolbar/static *
|
||||
44
README.md
Normal file
44
README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Flask-DebugToolbar
|
||||
|
||||
A [Flask][] extension that injects debugging information into rendered HTML
|
||||
pages. Presented as a sidebar with configurable panels of information.
|
||||
|
||||
This is a port of the excellent [django-debug-toolbar][ddt].
|
||||
|
||||
[Flask]: https://flask.palletsprojects.com
|
||||
[ddt]: https://github.com/jazzband/django-debug-toolbar/
|
||||
|
||||
|
||||
## Pallets Community Ecosystem
|
||||
|
||||
> [!IMPORTANT]\
|
||||
> This project is part of the Pallets Community Ecosystem. Pallets is the open
|
||||
> source organization that maintains Flask; Pallets-Eco enables community
|
||||
> maintenance of related projects. If you are interested in helping maintain
|
||||
> this project, please reach out on [the Pallets Discord server][discord].
|
||||
|
||||
[discord]: https://discord.gg/pallets
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
Setting up the debug toolbar is simple:
|
||||
|
||||
```python
|
||||
from flask import Flask
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["SECRET_KEY"] = "<replace with a secret key>"
|
||||
|
||||
toolbar = DebugToolbarExtension(app)
|
||||
```
|
||||
|
||||
The toolbar will automatically be injected into Jinja templates when debug
|
||||
mode is enabled.
|
||||
|
||||
```
|
||||
$ flask -A my_app run --debug
|
||||
```
|
||||
|
||||

|
||||
43
README.rst
43
README.rst
@@ -1,43 +0,0 @@
|
||||
Flask Debug-toolbar
|
||||
===================
|
||||
|
||||
This is a port of the excellent `django-debug-toolbar <https://github.com/django-debug-toolbar/django-debug-toolbar>`_
|
||||
for Flask applications.
|
||||
|
||||
.. image:: https://travis-ci.org/flask-debugtoolbar/flask-debugtoolbar.png?branch=master
|
||||
:target: https://travis-ci.org/flask-debugtoolbar/flask-debugtoolbar
|
||||
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
Installing is simple with pip::
|
||||
|
||||
$ pip install flask-debugtoolbar
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
Setting up the debug toolbar is simple::
|
||||
|
||||
from flask import Flask
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# the toolbar is only enabled in debug mode:
|
||||
app.debug = True
|
||||
|
||||
# set a 'SECRET_KEY' to enable the Flask session cookies
|
||||
app.config['SECRET_KEY'] = '<replace with a secret key>'
|
||||
|
||||
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.
|
||||
|
||||
See the `documentation`_ for more information.
|
||||
|
||||
.. _documentation: https://flask-debugtoolbar.readthedocs.io/
|
||||
157
docs/Makefile
157
docs/Makefile
@@ -1,157 +0,0 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = _build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html: _themes
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-DebugToolbar.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-DebugToolbar.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-DebugToolbar"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-DebugToolbar"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
||||
|
||||
_themes:
|
||||
git clone git://github.com/Pylons/pylons_sphinx_theme.git _themes
|
||||
cd ..; git submodule update --init; cd docs
|
||||
0
docs/_templates/.gitignore
vendored
0
docs/_templates/.gitignore
vendored
Submodule docs/_themes deleted from 1cc44686f0
280
docs/conf.py
280
docs/conf.py
@@ -1,261 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Flask-DebugToolbar documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed Feb 15 18:08:39 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
import importlib.metadata
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import pkg_resources
|
||||
import sys
|
||||
import time
|
||||
# Project --------------------------------------------------------------
|
||||
|
||||
import flask_debugtoolbar
|
||||
project = "Flask-DebugToolbar"
|
||||
version = release = importlib.metadata.version("flask-debugtoolbar").partition(".dev")[
|
||||
0
|
||||
]
|
||||
|
||||
# General --------------------------------------------------------------
|
||||
|
||||
BUILD_DATE = datetime.datetime.utcfromtimestamp(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
default_role = "code"
|
||||
extensions = [
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.intersphinx',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.extlinks",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinxcontrib.log_cabinet",
|
||||
"pallets_sphinx_themes",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Flask-DebugToolbar'
|
||||
copyright = u'2012-{0}'.format(BUILD_DATE.year)
|
||||
|
||||
# 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 full version, including alpha/beta/rc tags.
|
||||
release = flask_debugtoolbar.__version__
|
||||
# The short X.Y version.
|
||||
version = '.'.join(release.split('.')[:2])
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
autodoc_member_order = "bysource"
|
||||
autodoc_typehints = "description"
|
||||
autodoc_preserve_defaults = True
|
||||
extlinks = {
|
||||
"issue": ("https://github.com/pallets-eco/flask-debugtoolbar/issues/%s", "#%s"),
|
||||
"pr": ("https://github.com/pallets-eco/flask-debugtoolbar/pull/%s", "#%s"),
|
||||
}
|
||||
intersphinx_mapping = {
|
||||
'flasksqlalchemy': ('http://flask-sqlalchemy.pocoo.org/latest/', None)
|
||||
"python": ("https://docs.python.org/3/", None),
|
||||
"flasksqlalchemy": ("https://flask-sqlalchemy.palletsprojects.com", None),
|
||||
}
|
||||
|
||||
# HTML -----------------------------------------------------------------
|
||||
|
||||
# -- 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'
|
||||
|
||||
# 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 = {
|
||||
'index_logo': None,
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
sys.path.append(os.path.abspath('_themes'))
|
||||
html_theme_path = ['_themes']
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Flask-DebugToolbardoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'Flask-DebugToolbar.tex', u'Flask-DebugToolbar Documentation',
|
||||
u'Matt Good', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'flask-debugtoolbar', u'Flask-DebugToolbar Documentation',
|
||||
[u'Matt Good'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'Flask-DebugToolbar', u'Flask-DebugToolbar Documentation',
|
||||
u'Matt Good', 'Flask-DebugToolbar', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
html_theme = "flask"
|
||||
html_static_path = ["_static"]
|
||||
html_copy_source = False
|
||||
html_show_copyright = False
|
||||
html_use_index = False
|
||||
html_domain_indices = False
|
||||
|
||||
@@ -55,10 +55,19 @@ Name Description De
|
||||
==================================== ===================================== ==========================
|
||||
``DEBUG_TB_ENABLED`` Enable the toolbar? ``app.debug``
|
||||
``DEBUG_TB_HOSTS`` Whitelist of hosts to display toolbar any host
|
||||
``DEBUG_TB_ROUTES_HOST`` The host to associate with toolbar ``None``
|
||||
routes (where its assets are served
|
||||
from), or the sentinel value `*` to
|
||||
serve from the same host as the
|
||||
current request (ie any host). This
|
||||
is only required if Flask is
|
||||
configured to use `host_matching`.
|
||||
``DEBUG_TB_INTERCEPT_REDIRECTS`` Should intercept redirects? ``True``
|
||||
``DEBUG_TB_PANELS`` List of module/class names of panels enable all built-in panels
|
||||
``DEBUG_TB_PROFILER_ENABLED`` Enable the profiler on all requests ``False``, user-enabled
|
||||
``DEBUG_TB_TEMPLATE_EDITOR_ENABLED`` Enable the template editor ``False``
|
||||
``DEBUG_TB_PROFILER_DUMP_FILENAME`` Filename of the profiler stats dump, ``None``, no dump will be written
|
||||
can be a ``str`` or a ``callable``
|
||||
==================================== ===================================== ==========================
|
||||
|
||||
To change one of the config options, set it in the Flask app's config like::
|
||||
@@ -71,26 +80,19 @@ Panels
|
||||
|
||||
.. toctree::
|
||||
|
||||
panels
|
||||
panels
|
||||
license
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Fork us `on GitHub <https://github.com/mgood/flask-debugtoolbar>`_
|
||||
Fork us `on GitHub <https://github.com/pallets-eco/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
|
||||
.. _django-debug-toolbar: https://github.com/jazzband/django-debug-toolbar
|
||||
.. _Michael van Tellingen: https://github.com/mvantellingen
|
||||
.. _individual contributors: https://github.com/mgood/flask-debugtoolbar/graphs/contributors
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
.. _individual contributors: https://github.com/pallets-eco/flask-debugtoolbar/graphs/contributors
|
||||
|
||||
5
docs/license.rst
Normal file
5
docs/license.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
BSD-3-Clause License
|
||||
====================
|
||||
|
||||
.. literalinclude:: ../LICENSE.txt
|
||||
:language: text
|
||||
@@ -76,9 +76,9 @@ Shows SQL queries run during the current request.
|
||||
|
||||
.. image:: _static/screenshot-sqlalchemy-panel.png
|
||||
|
||||
.. _Flask-SQLAlchemy: http://flask-sqlalchemy.pocoo.org/
|
||||
.. _Flask-SQLAlchemy: https://flask-sqlalchemy.palletsprojects.com/
|
||||
|
||||
.. _Pygments: http://pygments.org/
|
||||
.. _Pygments: https://pygments.org/
|
||||
|
||||
|
||||
Logging
|
||||
|
||||
@@ -1,48 +1,50 @@
|
||||
# Run using: `FLASK_ENV=development flask run`
|
||||
# Run using: `FLASK_DEBUG=True flask run`
|
||||
|
||||
from flask import Flask, render_template, redirect, url_for
|
||||
from flask import Flask
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import url_for
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = True
|
||||
#app.config['DEBUG_TB_PANELS'] = (
|
||||
app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = True
|
||||
# app.config['DEBUG_TB_PANELS'] = (
|
||||
# 'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
|
||||
# 'flask_debugtoolbar.panels.logger.LoggingPanel',
|
||||
# 'flask_debugtoolbar.panels.timer.TimerDebugPanel',
|
||||
#)
|
||||
#app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' )
|
||||
app.config['SECRET_KEY'] = 'asd'
|
||||
# )
|
||||
# app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' )
|
||||
app.config["SECRET_KEY"] = "asd"
|
||||
app.config["SQLALCHEMY_RECORD_QUERIES"] = True
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/test.db"
|
||||
# This is no longer needed for Flask-SQLAlchemy 3.0+, if you're using 2.X you'll
|
||||
# want to define this:
|
||||
# app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
# TODO: This can be removed once flask_sqlalchemy 3.0 ships
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
toolbar = DebugToolbarExtension(app)
|
||||
|
||||
|
||||
class ExampleModel(db.Model):
|
||||
__tablename__ = 'examples'
|
||||
__tablename__ = "examples"
|
||||
value = db.Column(db.String(100), primary_key=True)
|
||||
|
||||
|
||||
@app.before_first_request
|
||||
def setup():
|
||||
db.create_all()
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
def index():
|
||||
app.logger.info("Hello there")
|
||||
ExampleModel.query.get(1)
|
||||
return render_template('index.html')
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route('/redirect')
|
||||
@app.route("/redirect")
|
||||
def redirect_example():
|
||||
response = redirect(url_for('index'))
|
||||
response.set_cookie('test_cookie', '1')
|
||||
response = redirect(url_for("index"))
|
||||
response.set_cookie("test_cookie", "1")
|
||||
return response
|
||||
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from flask import Blueprint, current_app, request, g, send_from_directory, url_for
|
||||
from flask.globals import _request_ctx_stack
|
||||
from jinja2 import __version__ as __jinja_version__
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from werkzeug.urls import url_quote_plus
|
||||
|
||||
from flask_debugtoolbar.compat import iteritems
|
||||
from flask_debugtoolbar.toolbar import DebugToolbar
|
||||
from flask_debugtoolbar.utils import decode_text, gzip_compress, gzip_decompress
|
||||
|
||||
try:
|
||||
# Python 3.8+
|
||||
from importlib.metadata import version
|
||||
|
||||
__version__ = version("Flask-DebugToolbar")
|
||||
except ImportError:
|
||||
import pkg_resources
|
||||
|
||||
__version__ = pkg_resources.get_distribution("Flask-DebugToolbar").version
|
||||
|
||||
|
||||
module = Blueprint('debugtoolbar', __name__)
|
||||
|
||||
|
||||
def replace_insensitive(string, target, replacement):
|
||||
"""Similar to string.replace() but is case insensitive
|
||||
Code borrowed from:
|
||||
http://forums.devshed.com/python-programming-11/case-insensitive-string-replace-490921.html
|
||||
"""
|
||||
no_case = string.lower()
|
||||
index = no_case.rfind(target.lower())
|
||||
if index >= 0:
|
||||
return string[:index] + replacement + string[index + len(target):]
|
||||
else: # no results so return the original string
|
||||
return string
|
||||
|
||||
|
||||
def _printable(value):
|
||||
try:
|
||||
return decode_text(repr(value))
|
||||
except Exception as e:
|
||||
return '<repr(%s) raised %s: %s>' % (
|
||||
object.__repr__(value), type(e).__name__, e)
|
||||
|
||||
|
||||
class DebugToolbarExtension(object):
|
||||
_static_dir = os.path.realpath(
|
||||
os.path.join(os.path.dirname(__file__), 'static'))
|
||||
|
||||
_redirect_codes = [301, 302, 303, 304]
|
||||
|
||||
def __init__(self, app=None):
|
||||
self.app = app
|
||||
self.debug_toolbars = {}
|
||||
jinja_extensions = ['jinja2.ext.i18n']
|
||||
|
||||
if __jinja_version__[0] == '2':
|
||||
jinja_extensions.append('jinja2.ext.with_')
|
||||
|
||||
# Configure jinja for the internal templates and add url rules
|
||||
# for static data
|
||||
self.jinja_env = Environment(
|
||||
autoescape=True,
|
||||
extensions=jinja_extensions,
|
||||
loader=PackageLoader(__name__, 'templates'))
|
||||
self.jinja_env.filters['urlencode'] = url_quote_plus
|
||||
self.jinja_env.filters['printable'] = _printable
|
||||
self.jinja_env.globals['url_for'] = url_for
|
||||
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
for k, v in iteritems(self._default_config(app)):
|
||||
app.config.setdefault(k, v)
|
||||
|
||||
if not app.config['DEBUG_TB_ENABLED']:
|
||||
return
|
||||
|
||||
if not app.config.get('SECRET_KEY'):
|
||||
raise RuntimeError(
|
||||
"The Flask-DebugToolbar requires the 'SECRET_KEY' config "
|
||||
"var to be set")
|
||||
|
||||
DebugToolbar.load_panels(app)
|
||||
|
||||
app.before_request(self.process_request)
|
||||
app.after_request(self.process_response)
|
||||
app.teardown_request(self.teardown_request)
|
||||
|
||||
# Monkey-patch the Flask.dispatch_request method
|
||||
app.dispatch_request = self.dispatch_request
|
||||
|
||||
app.add_url_rule('/_debug_toolbar/static/<path:filename>',
|
||||
'_debug_toolbar.static', self.send_static_file)
|
||||
|
||||
app.register_blueprint(module, url_prefix='/_debug_toolbar/views')
|
||||
|
||||
def _default_config(self, app):
|
||||
return {
|
||||
'DEBUG_TB_ENABLED': app.debug,
|
||||
'DEBUG_TB_HOSTS': (),
|
||||
'DEBUG_TB_INTERCEPT_REDIRECTS': True,
|
||||
'DEBUG_TB_PANELS': (
|
||||
'flask_debugtoolbar.panels.versions.VersionDebugPanel',
|
||||
'flask_debugtoolbar.panels.timer.TimerDebugPanel',
|
||||
'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
|
||||
'flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel',
|
||||
'flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel',
|
||||
'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',
|
||||
'flask_debugtoolbar.panels.g.GDebugPanel',
|
||||
),
|
||||
}
|
||||
|
||||
def dispatch_request(self):
|
||||
"""Modified version of Flask.dispatch_request to call process_view."""
|
||||
req = _request_ctx_stack.top.request
|
||||
app = current_app
|
||||
|
||||
if req.routing_exception is not None:
|
||||
app.raise_routing_exception(req)
|
||||
|
||||
rule = req.url_rule
|
||||
|
||||
# if we provide automatic options for this URL and the
|
||||
# request came with the OPTIONS method, reply automatically
|
||||
if getattr(rule, 'provide_automatic_options', False) \
|
||||
and req.method == 'OPTIONS':
|
||||
return app.make_default_options_response()
|
||||
|
||||
# otherwise dispatch to the handler for that endpoint
|
||||
view_func = app.view_functions[rule.endpoint]
|
||||
view_func = self.process_view(app, view_func, req.view_args)
|
||||
|
||||
return view_func(**req.view_args)
|
||||
|
||||
def _show_toolbar(self):
|
||||
"""Return a boolean to indicate if we need to show the toolbar."""
|
||||
if request.blueprint == 'debugtoolbar':
|
||||
return False
|
||||
|
||||
hosts = current_app.config['DEBUG_TB_HOSTS']
|
||||
if hosts and request.remote_addr not in hosts:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def send_static_file(self, filename):
|
||||
"""Send a static file from the flask-debugtoolbar static directory."""
|
||||
return send_from_directory(self._static_dir, filename)
|
||||
|
||||
def process_request(self):
|
||||
g.debug_toolbar = self
|
||||
|
||||
if not self._show_toolbar():
|
||||
return
|
||||
|
||||
real_request = request._get_current_object()
|
||||
|
||||
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)
|
||||
|
||||
def process_view(self, app, view_func, view_kwargs):
|
||||
""" This method is called just before the flask view is called.
|
||||
This is done by the dispatch_request method.
|
||||
"""
|
||||
real_request = request._get_current_object()
|
||||
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):
|
||||
real_request = request._get_current_object()
|
||||
if real_request not in self.debug_toolbars:
|
||||
return response
|
||||
|
||||
# Intercept http redirect codes and display an html page with a
|
||||
# link to the target.
|
||||
if current_app.config['DEBUG_TB_INTERCEPT_REDIRECTS']:
|
||||
if response.status_code in self._redirect_codes:
|
||||
redirect_to = response.location
|
||||
redirect_code = response.status_code
|
||||
if redirect_to:
|
||||
content = self.render('redirect.html', {
|
||||
'redirect_to': redirect_to,
|
||||
'redirect_code': redirect_code
|
||||
})
|
||||
response.content_length = len(content)
|
||||
response.location = None
|
||||
response.response = [content]
|
||||
response.status_code = 200
|
||||
|
||||
# If the http response code is 200 then we process to add the
|
||||
# toolbar to the returned html response.
|
||||
if not (response.status_code == 200 and
|
||||
response.is_sequence and
|
||||
response.headers['content-type'].startswith('text/html')):
|
||||
return response
|
||||
|
||||
if 'gzip' in response.headers.get('Content-Encoding', ''):
|
||||
response_html = gzip_decompress(response.data).decode(response.charset)
|
||||
else:
|
||||
response_html = response.data.decode(response.charset)
|
||||
|
||||
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)
|
||||
if 'gzip' in response.headers.get('Content-Encoding', ''):
|
||||
content = gzip_compress(content)
|
||||
response.response = [content]
|
||||
response.content_length = len(content)
|
||||
|
||||
return response
|
||||
|
||||
def teardown_request(self, exc):
|
||||
self.debug_toolbars.pop(request._get_current_object(), None)
|
||||
|
||||
def render(self, template_name, context):
|
||||
template = self.jinja_env.get_template(template_name)
|
||||
return template.render(**context)
|
||||
@@ -1,9 +0,0 @@
|
||||
import sys
|
||||
|
||||
PY2 = sys.version_info[0] == 2
|
||||
|
||||
|
||||
if PY2:
|
||||
iteritems = lambda d: d.iteritems()
|
||||
else:
|
||||
iteritems = lambda d: iter(d.items())
|
||||
@@ -1,61 +0,0 @@
|
||||
"""Base DebugPanel class"""
|
||||
|
||||
|
||||
class DebugPanel(object):
|
||||
"""
|
||||
Base class for debug panels.
|
||||
"""
|
||||
# name = Base
|
||||
|
||||
# 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
|
||||
|
||||
# We'll maintain a local context instance so we can expose our template
|
||||
# context variables to panels which need them:
|
||||
context = {}
|
||||
|
||||
# Panel methods
|
||||
def __init__(self, jinja_env, context={}):
|
||||
self.context.update(context)
|
||||
self.jinja_env = jinja_env
|
||||
|
||||
# If the client enabled the panel
|
||||
self.is_active = False
|
||||
|
||||
def render(self, template_name, context):
|
||||
template = self.jinja_env.get_template(template_name)
|
||||
return template.render(**context)
|
||||
|
||||
def dom_id(self):
|
||||
return 'flDebug%sPanel' % (self.name.replace(' ', ''))
|
||||
|
||||
def nav_title(self):
|
||||
"""Title showing in toolbar"""
|
||||
raise NotImplementedError
|
||||
|
||||
def nav_subtitle(self):
|
||||
"""Subtitle showing until title in toolbar"""
|
||||
return ''
|
||||
|
||||
def title(self):
|
||||
"""Title showing in panel"""
|
||||
raise NotImplementedError
|
||||
|
||||
def url(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def content(self):
|
||||
raise NotImplementedError
|
||||
|
||||
# Standard middleware methods
|
||||
def process_request(self, request):
|
||||
pass
|
||||
|
||||
def process_view(self, request, view_func, view_kwargs):
|
||||
pass
|
||||
|
||||
def process_response(self, request, response):
|
||||
pass
|
||||
@@ -1,29 +0,0 @@
|
||||
from flask import current_app
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
class ConfigVarsDebugPanel(DebugPanel):
|
||||
"""
|
||||
A panel to display all variables from Flask configuration
|
||||
"""
|
||||
name = 'ConfigVars'
|
||||
has_content = True
|
||||
|
||||
def nav_title(self):
|
||||
return _('Config')
|
||||
|
||||
def title(self):
|
||||
return _('Config')
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def content(self):
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'config': current_app.config,
|
||||
})
|
||||
|
||||
return self.render('panels/config_vars.html', context)
|
||||
@@ -1,29 +0,0 @@
|
||||
from flask import g
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
class GDebugPanel(DebugPanel):
|
||||
"""
|
||||
A panel to display flask.g content.
|
||||
"""
|
||||
name = 'g'
|
||||
has_content = True
|
||||
|
||||
def nav_title(self):
|
||||
return _('flask.g')
|
||||
|
||||
def title(self):
|
||||
return _('flask.g content')
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def content(self):
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'g_content': g.__dict__
|
||||
})
|
||||
return self.render('panels/g.html', context)
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
class HeaderDebugPanel(DebugPanel):
|
||||
"""
|
||||
A panel to display HTTP headers.
|
||||
"""
|
||||
name = 'Header'
|
||||
has_content = True
|
||||
# List of headers we want to display
|
||||
header_filter = (
|
||||
'CONTENT_TYPE',
|
||||
'HTTP_ACCEPT',
|
||||
'HTTP_ACCEPT_CHARSET',
|
||||
'HTTP_ACCEPT_ENCODING',
|
||||
'HTTP_ACCEPT_LANGUAGE',
|
||||
'HTTP_CACHE_CONTROL',
|
||||
'HTTP_CONNECTION',
|
||||
'HTTP_HOST',
|
||||
'HTTP_KEEP_ALIVE',
|
||||
'HTTP_REFERER',
|
||||
'HTTP_USER_AGENT',
|
||||
'QUERY_STRING',
|
||||
'REMOTE_ADDR',
|
||||
'REMOTE_HOST',
|
||||
'REQUEST_METHOD',
|
||||
'SCRIPT_NAME',
|
||||
'SERVER_NAME',
|
||||
'SERVER_PORT',
|
||||
'SERVER_PROTOCOL',
|
||||
'SERVER_SOFTWARE',
|
||||
)
|
||||
|
||||
def nav_title(self):
|
||||
return _('HTTP Headers')
|
||||
|
||||
def title(self):
|
||||
return _('HTTP Headers')
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def process_request(self, request):
|
||||
self.headers = dict(
|
||||
[(k, request.environ[k])
|
||||
for k in self.header_filter if k in request.environ]
|
||||
)
|
||||
|
||||
def content(self):
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'headers': self.headers
|
||||
})
|
||||
return self.render('panels/headers.html', context)
|
||||
@@ -1,115 +0,0 @@
|
||||
from __future__ import with_statement
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
try:
|
||||
import threading
|
||||
except ImportError:
|
||||
threading = None
|
||||
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from flask_debugtoolbar.utils import format_fname
|
||||
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
class ThreadTrackingHandler(logging.Handler):
|
||||
def __init__(self):
|
||||
if threading is None:
|
||||
raise NotImplementedError("threading module is not available, \
|
||||
the logging panel cannot be used without it")
|
||||
logging.Handler.__init__(self)
|
||||
self.records = {} # a dictionary that maps threads to log records
|
||||
|
||||
def emit(self, record):
|
||||
self.get_records().append(record)
|
||||
|
||||
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.
|
||||
"""
|
||||
if thread is None:
|
||||
thread = threading.currentThread()
|
||||
if thread not in self.records:
|
||||
self.records[thread] = []
|
||||
return self.records[thread]
|
||||
|
||||
def clear_records(self, thread=None):
|
||||
if thread is None:
|
||||
thread = threading.currentThread()
|
||||
if thread in self.records:
|
||||
del self.records[thread]
|
||||
|
||||
|
||||
handler = None
|
||||
_init_lock = threading.Lock()
|
||||
|
||||
|
||||
def _init_once():
|
||||
global handler
|
||||
if handler is not None:
|
||||
return
|
||||
with _init_lock:
|
||||
if handler is not None:
|
||||
return
|
||||
|
||||
# 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.
|
||||
try:
|
||||
from werkzeug._internal import _log
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
_log('debug', 'Initializing Flask-DebugToolbar log handler')
|
||||
|
||||
handler = ThreadTrackingHandler()
|
||||
logging.root.addHandler(handler)
|
||||
|
||||
|
||||
class LoggingPanel(DebugPanel):
|
||||
name = 'Logging'
|
||||
has_content = True
|
||||
|
||||
def process_request(self, request):
|
||||
_init_once()
|
||||
handler.clear_records()
|
||||
|
||||
def get_and_delete(self):
|
||||
records = handler.get_records()
|
||||
handler.clear_records()
|
||||
return records
|
||||
|
||||
def nav_title(self):
|
||||
return _("Logging")
|
||||
|
||||
def nav_subtitle(self):
|
||||
# FIXME l10n: use ngettext
|
||||
num_records = len(handler.get_records())
|
||||
return '%s message%s' % (num_records, '' if num_records == 1 else 's')
|
||||
|
||||
def title(self):
|
||||
return _('Log Messages')
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def content(self):
|
||||
records = []
|
||||
for record in self.get_and_delete():
|
||||
records.append({
|
||||
'message': record.getMessage(),
|
||||
'time': datetime.datetime.fromtimestamp(record.created),
|
||||
'level': record.levelname,
|
||||
'file': format_fname(record.pathname),
|
||||
'file_long': record.pathname,
|
||||
'line': record.lineno,
|
||||
})
|
||||
|
||||
context = self.context.copy()
|
||||
context.update({'records': records})
|
||||
|
||||
return self.render('panels/logger.html', context)
|
||||
@@ -1,118 +0,0 @@
|
||||
try:
|
||||
import cProfile as profile
|
||||
except ImportError:
|
||||
import profile
|
||||
import functools
|
||||
import pstats
|
||||
|
||||
from flask import current_app
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from flask_debugtoolbar.utils import format_fname
|
||||
|
||||
|
||||
class ProfilerDebugPanel(DebugPanel):
|
||||
"""
|
||||
Panel that displays the time a response took with cProfile output.
|
||||
"""
|
||||
name = 'Profiler'
|
||||
|
||||
user_activate = True
|
||||
|
||||
def __init__(self, jinja_env, context={}):
|
||||
DebugPanel.__init__(self, jinja_env, context=context)
|
||||
if current_app.config.get('DEBUG_TB_PROFILER_ENABLED'):
|
||||
self.is_active = True
|
||||
|
||||
def has_content(self):
|
||||
return bool(self.profiler)
|
||||
|
||||
def process_request(self, request):
|
||||
if not self.is_active:
|
||||
return
|
||||
|
||||
self.profiler = profile.Profile()
|
||||
self.stats = None
|
||||
|
||||
def process_view(self, request, view_func, view_kwargs):
|
||||
if self.is_active:
|
||||
func = functools.partial(self.profiler.runcall, view_func)
|
||||
functools.update_wrapper(func, view_func)
|
||||
return func
|
||||
|
||||
def process_response(self, request, response):
|
||||
if not self.is_active:
|
||||
return False
|
||||
|
||||
if self.profiler is not None:
|
||||
self.profiler.disable()
|
||||
try:
|
||||
stats = pstats.Stats(self.profiler)
|
||||
except TypeError:
|
||||
self.is_active = False
|
||||
return False
|
||||
function_calls = []
|
||||
for func in stats.sort_stats(1).fcn_list:
|
||||
current = {}
|
||||
info = stats.stats[func]
|
||||
|
||||
# Number of calls
|
||||
if info[0] != info[1]:
|
||||
current['ncalls'] = '%d/%d' % (info[1], info[0])
|
||||
else:
|
||||
current['ncalls'] = info[1]
|
||||
|
||||
# Total time
|
||||
current['tottime'] = info[2] * 1000
|
||||
|
||||
# Quotient of total time divided by number of calls
|
||||
if info[1]:
|
||||
current['percall'] = info[2] * 1000 / info[1]
|
||||
else:
|
||||
current['percall'] = 0
|
||||
|
||||
# Cumulative time
|
||||
current['cumtime'] = info[3] * 1000
|
||||
|
||||
# Quotient of the cumulative time divded by the number of
|
||||
# primitive calls.
|
||||
if info[0]:
|
||||
current['percall_cum'] = info[3] * 1000 / info[0]
|
||||
else:
|
||||
current['percall_cum'] = 0
|
||||
|
||||
# Filename
|
||||
filename = pstats.func_std_string(func)
|
||||
current['filename_long'] = filename
|
||||
current['filename'] = format_fname(filename)
|
||||
function_calls.append(current)
|
||||
|
||||
self.stats = stats
|
||||
self.function_calls = function_calls
|
||||
# destroy the profiler just in case
|
||||
return response
|
||||
|
||||
def title(self):
|
||||
if not self.is_active:
|
||||
return "Profiler not active"
|
||||
return 'View: %.2fms' % (float(self.stats.total_tt)*1000,)
|
||||
|
||||
def nav_title(self):
|
||||
return 'Profiler'
|
||||
|
||||
def nav_subtitle(self):
|
||||
if not self.is_active:
|
||||
return "in-active"
|
||||
return 'View: %.2fms' % (float(self.stats.total_tt)*1000,)
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def content(self):
|
||||
if not self.is_active:
|
||||
return "The profiler is not activated, activate it to use it"
|
||||
|
||||
context = {
|
||||
'stats': self.stats,
|
||||
'function_calls': self.function_calls,
|
||||
}
|
||||
return self.render('panels/profiler.html', context)
|
||||
@@ -1,49 +0,0 @@
|
||||
from flask import session
|
||||
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
class RequestVarsDebugPanel(DebugPanel):
|
||||
"""
|
||||
A panel to display request variables (POST/GET, session, cookies).
|
||||
"""
|
||||
name = 'RequestVars'
|
||||
has_content = True
|
||||
|
||||
def nav_title(self):
|
||||
return _('Request Vars')
|
||||
|
||||
def title(self):
|
||||
return _('Request Vars')
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def process_request(self, request):
|
||||
self.request = request
|
||||
self.session = session
|
||||
self.view_func = None
|
||||
self.view_args = []
|
||||
self.view_kwargs = {}
|
||||
|
||||
def process_view(self, request, view_func, view_kwargs):
|
||||
self.view_func = view_func
|
||||
self.view_kwargs = view_kwargs
|
||||
|
||||
def content(self):
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'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 {},
|
||||
'session': self.session.items(),
|
||||
})
|
||||
|
||||
return self.render('panels/request_vars.html', context)
|
||||
@@ -1,38 +0,0 @@
|
||||
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 = [
|
||||
rule
|
||||
for rule in current_app.url_map.iter_rules()
|
||||
if not rule.rule.startswith('/_debug_toolbar')
|
||||
]
|
||||
|
||||
def content(self):
|
||||
return self.render('panels/route_list.html', {
|
||||
'routes': self.routes,
|
||||
})
|
||||
@@ -1,148 +0,0 @@
|
||||
try:
|
||||
from flask_sqlalchemy import get_debug_queries, SQLAlchemy
|
||||
except ImportError:
|
||||
sqlalchemy_available = False
|
||||
get_debug_queries = SQLAlchemy = None
|
||||
else:
|
||||
sqlalchemy_available = True
|
||||
|
||||
from flask import request, current_app, abort, g
|
||||
from flask_debugtoolbar import module
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from flask_debugtoolbar.utils import format_fname, format_sql
|
||||
import itsdangerous
|
||||
|
||||
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
def query_signer():
|
||||
return itsdangerous.URLSafeSerializer(current_app.config['SECRET_KEY'],
|
||||
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 is_select(statement):
|
||||
return None
|
||||
|
||||
try:
|
||||
return query_signer().dumps([statement, params])
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
|
||||
def load_query(data):
|
||||
try:
|
||||
statement, params = query_signer().loads(request.args['query'])
|
||||
except (itsdangerous.BadSignature, TypeError):
|
||||
abort(406)
|
||||
|
||||
# Make sure it is a select statement
|
||||
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 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.
|
||||
"""
|
||||
name = 'SQLAlchemy'
|
||||
|
||||
@property
|
||||
def has_content(self):
|
||||
return bool(get_queries()) or not is_available()
|
||||
|
||||
def process_request(self, request):
|
||||
pass
|
||||
|
||||
def process_response(self, request, response):
|
||||
pass
|
||||
|
||||
def nav_title(self):
|
||||
return _('SQLAlchemy')
|
||||
|
||||
def nav_subtitle(self):
|
||||
count = len(get_queries())
|
||||
|
||||
if not count and not is_available():
|
||||
return 'Unavailable'
|
||||
|
||||
return '%d %s' % (count, 'query' if count == 1 else 'queries')
|
||||
|
||||
def title(self):
|
||||
return _('SQLAlchemy queries')
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def content(self):
|
||||
queries = get_queries()
|
||||
|
||||
if not queries and not is_available():
|
||||
return self.render('panels/sqlalchemy_error.html', {
|
||||
'sqlalchemy_available': sqlalchemy_available,
|
||||
'extension_used': extension_used(),
|
||||
'recording_enabled': recording_enabled(),
|
||||
})
|
||||
|
||||
data = []
|
||||
for query in queries:
|
||||
data.append({
|
||||
'duration': query.duration,
|
||||
'sql': format_sql(query.statement, query.parameters),
|
||||
'signed_query': dump_query(query.statement, query.parameters),
|
||||
'context_long': query.context,
|
||||
'context': format_fname(query.context)
|
||||
})
|
||||
return self.render('panels/sqlalchemy.html', {'queries': data})
|
||||
|
||||
# Panel views
|
||||
|
||||
|
||||
@module.route('/sqlalchemy/sql_select', methods=['GET', 'POST'])
|
||||
@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(),
|
||||
'headers': result.keys(),
|
||||
'sql': format_sql(statement, params),
|
||||
'duration': float(request.args['duration']),
|
||||
})
|
||||
@@ -1,136 +0,0 @@
|
||||
import collections
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from flask import (
|
||||
template_rendered, request, g,
|
||||
Response, current_app, abort, url_for
|
||||
)
|
||||
from flask_debugtoolbar import module
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
class TemplateDebugPanel(DebugPanel):
|
||||
"""
|
||||
Panel that displays the time a response took in milliseconds.
|
||||
"""
|
||||
name = 'Template'
|
||||
has_content = True
|
||||
|
||||
# save the context for the 5 most recent requests
|
||||
template_cache = collections.deque(maxlen=5)
|
||||
|
||||
@classmethod
|
||||
def get_cache_for_key(self, key):
|
||||
for cache_key, value in self.template_cache:
|
||||
if key == cache_key:
|
||||
return value
|
||||
raise KeyError(key)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(self.__class__, self).__init__(*args, **kwargs)
|
||||
self.key = str(uuid.uuid4())
|
||||
self.templates = []
|
||||
template_rendered.connect(self._store_template_info)
|
||||
|
||||
def _store_template_info(self, sender, **kwargs):
|
||||
# only record in the cache if the editor is enabled and there is
|
||||
# actually a template for this request
|
||||
if not self.templates and is_editor_enabled():
|
||||
self.template_cache.append((self.key, self.templates))
|
||||
self.templates.append(kwargs)
|
||||
|
||||
def process_request(self, request):
|
||||
pass
|
||||
|
||||
def process_response(self, request, response):
|
||||
pass
|
||||
|
||||
def nav_title(self):
|
||||
return _('Templates')
|
||||
|
||||
def nav_subtitle(self):
|
||||
return "%d rendered" % len(self.templates)
|
||||
|
||||
def title(self):
|
||||
return _('Templates')
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def content(self):
|
||||
return self.render('panels/template.html', {
|
||||
'key': self.key,
|
||||
'templates': self.templates,
|
||||
'editable': is_editor_enabled(),
|
||||
})
|
||||
|
||||
|
||||
def is_editor_enabled():
|
||||
return current_app.config.get('DEBUG_TB_TEMPLATE_EDITOR_ENABLED')
|
||||
|
||||
|
||||
def require_enabled():
|
||||
if not is_editor_enabled():
|
||||
abort(403)
|
||||
|
||||
|
||||
def _get_source(template):
|
||||
with open(template.filename, 'rb') as fp:
|
||||
source = fp.read()
|
||||
return source.decode(_template_encoding())
|
||||
|
||||
|
||||
def _template_encoding():
|
||||
return getattr(current_app.jinja_loader, 'encoding', 'utf-8')
|
||||
|
||||
|
||||
@module.route('/template/<key>')
|
||||
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)]
|
||||
return g.debug_toolbar.render('panels/template_editor.html', {
|
||||
'static_path': url_for('_debug_toolbar.static', filename=''),
|
||||
'request': request,
|
||||
'templates': [
|
||||
{'name': t.name, 'source': _get_source(t)}
|
||||
for t in templates
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@module.route('/template/<key>/save', methods=['POST'])
|
||||
def save_template(key):
|
||||
require_enabled()
|
||||
template = TemplateDebugPanel.get_cache_for_key(key)[0]['template']
|
||||
content = request.form['content'].encode(_template_encoding())
|
||||
with open(template.filename, 'wb') as fp:
|
||||
fp.write(content)
|
||||
return 'ok'
|
||||
|
||||
|
||||
@module.route('/template/<key>', methods=['POST'])
|
||||
def template_preview(key):
|
||||
require_enabled()
|
||||
context = TemplateDebugPanel.get_cache_for_key(key)[0]['context']
|
||||
content = request.form['content']
|
||||
env = current_app.jinja_env.overlay(autoescape=True)
|
||||
try:
|
||||
template = env.from_string(content)
|
||||
return template.render(context)
|
||||
except Exception as e:
|
||||
tb = sys.exc_info()[2]
|
||||
try:
|
||||
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')
|
||||
finally:
|
||||
del tb
|
||||
@@ -1,96 +0,0 @@
|
||||
try:
|
||||
import resource
|
||||
except ImportError:
|
||||
pass # Will fail on Win32 systems
|
||||
import time
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
class TimerDebugPanel(DebugPanel):
|
||||
"""
|
||||
Panel that displays the time a response took in milliseconds.
|
||||
"""
|
||||
name = 'Timer'
|
||||
try: # if resource module not available, don't show content panel
|
||||
resource
|
||||
except NameError:
|
||||
has_content = False
|
||||
has_resource = False
|
||||
else:
|
||||
has_content = True
|
||||
has_resource = True
|
||||
|
||||
def process_request(self, request):
|
||||
self._start_time = time.time()
|
||||
if self.has_resource:
|
||||
self._start_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
|
||||
def process_response(self, request, response):
|
||||
self.total_time = (time.time() - self._start_time) * 1000
|
||||
if self.has_resource:
|
||||
self._end_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
|
||||
def nav_title(self):
|
||||
return _('Time')
|
||||
|
||||
def nav_subtitle(self):
|
||||
# TODO l10n
|
||||
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')
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def _elapsed_ru(self, name):
|
||||
return (getattr(self._end_rusage, name)
|
||||
- getattr(self._start_rusage, name))
|
||||
|
||||
def content(self):
|
||||
|
||||
utime = 1000 * self._elapsed_ru('ru_utime')
|
||||
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')
|
||||
|
||||
# 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
|
||||
# I noticed that I was getting nothing but zeroes and that the docs agreed. :-(
|
||||
#
|
||||
# blkin = self._elapsed_ru('ru_inblock')
|
||||
# blkout = self._elapsed_ru('ru_oublock')
|
||||
# swap = self._elapsed_ru('ru_nswap')
|
||||
# rss = self._end_rusage.ru_maxrss
|
||||
# srss = self._end_rusage.ru_ixrss
|
||||
# urss = self._end_rusage.ru_idrss
|
||||
# usrss = self._end_rusage.ru_isrss
|
||||
|
||||
# TODO l10n on values
|
||||
rows = (
|
||||
(_('User CPU time'), '%0.3f msec' % utime),
|
||||
(_('System CPU time'), '%0.3f msec' % stime),
|
||||
(_('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)),
|
||||
)
|
||||
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'rows': rows,
|
||||
})
|
||||
|
||||
return self.render('panels/timer.html', context)
|
||||
@@ -1,52 +0,0 @@
|
||||
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 = True
|
||||
|
||||
def nav_title(self):
|
||||
return _('Versions')
|
||||
|
||||
def nav_subtitle(self):
|
||||
return 'Flask %s' % flask_version
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def title(self):
|
||||
return _('Versions')
|
||||
|
||||
def content(self):
|
||||
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,
|
||||
})
|
||||
16
flask_debugtoolbar/static/js/jquery.js
vendored
16
flask_debugtoolbar/static/js/jquery.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,33 +0,0 @@
|
||||
<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>
|
||||
@@ -1,77 +0,0 @@
|
||||
try:
|
||||
from urllib.parse import unquote
|
||||
except ImportError:
|
||||
from urllib import unquote
|
||||
|
||||
from flask import url_for, current_app
|
||||
from werkzeug.utils import import_string
|
||||
|
||||
|
||||
class DebugToolbar(object):
|
||||
|
||||
_cached_panel_classes = {}
|
||||
|
||||
def __init__(self, request, jinja_env):
|
||||
self.jinja_env = jinja_env
|
||||
self.request = request
|
||||
self.panels = []
|
||||
|
||||
self.template_context = {
|
||||
'static_path': url_for('_debug_toolbar.static', filename='')
|
||||
}
|
||||
|
||||
self.create_panels()
|
||||
|
||||
def create_panels(self):
|
||||
"""
|
||||
Populate debug panels
|
||||
"""
|
||||
activated = self.request.cookies.get('fldt_active', '')
|
||||
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)
|
||||
|
||||
if panel_instance.dom_id() in activated:
|
||||
panel_instance.is_active = True
|
||||
|
||||
self.panels.append(panel_instance)
|
||||
|
||||
def render_toolbar(self):
|
||||
context = self.template_context.copy()
|
||||
context.update({'panels': self.panels})
|
||||
|
||||
template = self.jinja_env.get_template('base.html')
|
||||
return template.render(**context)
|
||||
|
||||
@classmethod
|
||||
def load_panels(cls, app):
|
||||
for panel_class in cls._iter_panels(app):
|
||||
# just loop to make sure they've been loaded
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _iter_panels(cls, app):
|
||||
for panel_path in app.config['DEBUG_TB_PANELS']:
|
||||
panel_class = cls._import_panel(app, panel_path)
|
||||
if panel_class is not None:
|
||||
yield panel_class
|
||||
|
||||
@classmethod
|
||||
def _import_panel(cls, app, path):
|
||||
cache = cls._cached_panel_classes
|
||||
|
||||
try:
|
||||
return cache[path]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
panel_class = import_string(path)
|
||||
except ImportError as e:
|
||||
app.logger.warning('Disabled %s due to ImportError: %s', path, e)
|
||||
panel_class = None
|
||||
|
||||
cache[path] = panel_class
|
||||
return panel_class
|
||||
@@ -1,6 +1,84 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel"
|
||||
[project]
|
||||
name = "Flask-DebugToolbar"
|
||||
version = "0.16.0"
|
||||
description = "A toolbar overlay for debugging Flask applications."
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE.txt" }
|
||||
author = [{ name = "Michael van Tellingen" }]
|
||||
maintainers = [{ name = "Pallets Ecosystem", email = "contact@palletsprojects.com" }]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Framework :: Flask",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Programming Language :: Python",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"flask>=2.3.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://flask-debugtoolbar.readthedocs.io"
|
||||
Changes = "https://github.com/pallets-eco/flask-debugtoolbar/releases/"
|
||||
Source = "https://github.com/pallets-eco/flask-debugtoolbar/"
|
||||
Chat = "https://discord.gg/pallets"
|
||||
|
||||
[build-system]
|
||||
requires = ["flit_core<4"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
|
||||
[tool.flit.module]
|
||||
name = "flask_debugtoolbar"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
filterwarnings = [
|
||||
"error",
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
source = ["flask_debugtoolbar", "tests"]
|
||||
|
||||
[tool.coverage.paths]
|
||||
source = ["src", "*/site-packages"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
files = ["src/flask_debugtoolbar", "tests"]
|
||||
show_error_codes = true
|
||||
pretty = true
|
||||
strict = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"sqlparse.*"
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.8"
|
||||
include = ["src/flask_debugtoolbar", "tests"]
|
||||
typeCheckingMode = "basic"
|
||||
|
||||
[tool.ruff]
|
||||
src = ["src"]
|
||||
fix = true
|
||||
show-fixes = true
|
||||
output-format = "full"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"B", # flake8-bugbear
|
||||
"E", # pycodestyle error
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle warning
|
||||
]
|
||||
ignore-init-module-imports = true
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-single-line = true
|
||||
order-by-type = false
|
||||
|
||||
1
requirements/build.in
Normal file
1
requirements/build.in
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
18
requirements/build.txt
Normal file
18
requirements/build.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile build.in
|
||||
#
|
||||
build==1.2.2
|
||||
# via -r build.in
|
||||
importlib-metadata==7.1.0
|
||||
# via build
|
||||
packaging==24.0
|
||||
# via build
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
tomli==2.0.1
|
||||
# via build
|
||||
zipp==3.19.1
|
||||
# via importlib-metadata
|
||||
5
requirements/dev.in
Normal file
5
requirements/dev.in
Normal file
@@ -0,0 +1,5 @@
|
||||
-r docs.txt
|
||||
-r tests.txt
|
||||
-r typing.txt
|
||||
pre-commit
|
||||
tox
|
||||
257
requirements/dev.txt
Normal file
257
requirements/dev.txt
Normal file
@@ -0,0 +1,257 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile dev.in
|
||||
#
|
||||
alabaster==0.7.13
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
babel==2.14.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
blinker==1.8.1
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
cachetools==5.3.3
|
||||
# via tox
|
||||
certifi==2024.7.4
|
||||
# via
|
||||
# -r docs.txt
|
||||
# requests
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
chardet==5.2.0
|
||||
# via tox
|
||||
charset-normalizer==3.3.2
|
||||
# via
|
||||
# -r docs.txt
|
||||
# requests
|
||||
click==8.1.7
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
colorama==0.4.6
|
||||
# via tox
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
docutils==0.20.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
exceptiongroup==1.2.1
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# pytest
|
||||
filelock==3.14.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
flask==3.0.3
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask-sqlalchemy
|
||||
flask-sqlalchemy==3.1.1
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
greenlet==3.0.3
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# sqlalchemy
|
||||
identify==2.5.36
|
||||
# via pre-commit
|
||||
idna==3.7
|
||||
# via
|
||||
# -r docs.txt
|
||||
# requests
|
||||
imagesize==1.4.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
importlib-metadata==7.1.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
# sphinx
|
||||
iniconfig==2.0.0
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# pytest
|
||||
itsdangerous==2.2.0
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
jinja2==3.1.4
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
# sphinx
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# jinja2
|
||||
# werkzeug
|
||||
mypy==1.11.2
|
||||
# via -r typing.txt
|
||||
mypy-extensions==1.0.0
|
||||
# via
|
||||
# -r typing.txt
|
||||
# mypy
|
||||
nodeenv==1.8.0
|
||||
# via
|
||||
# -r typing.txt
|
||||
# pre-commit
|
||||
# pyright
|
||||
packaging==24.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# pallets-sphinx-themes
|
||||
# pyproject-api
|
||||
# pytest
|
||||
# sphinx
|
||||
# tox
|
||||
pallets-sphinx-themes==2.1.3
|
||||
# via -r docs.txt
|
||||
platformdirs==4.2.1
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
pluggy==1.5.0
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# pytest
|
||||
# tox
|
||||
pre-commit==3.5.0
|
||||
# via -r dev.in
|
||||
pygments==2.18.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# sphinx
|
||||
pyproject-api==1.6.1
|
||||
# via tox
|
||||
pyright==1.1.382.post1
|
||||
# via -r typing.txt
|
||||
pytest==8.3.3
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
pytz==2024.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# babel
|
||||
pyyaml==6.0.1
|
||||
# via pre-commit
|
||||
requests==2.32.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
snowballstemmer==2.2.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinx==7.1.2
|
||||
# via
|
||||
# -r docs.txt
|
||||
# pallets-sphinx-themes
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinxcontrib-applehelp==1.0.4
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r docs.txt
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sqlalchemy==2.0.29
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask-sqlalchemy
|
||||
tomli==2.0.1
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# mypy
|
||||
# pyproject-api
|
||||
# pytest
|
||||
# tox
|
||||
tox==4.15.1
|
||||
# via -r dev.in
|
||||
types-docutils==0.21.0.20240423
|
||||
# via
|
||||
# -r typing.txt
|
||||
# types-pygments
|
||||
types-pygments==2.18.0.20240506
|
||||
# via -r typing.txt
|
||||
types-setuptools==69.5.0.20240423
|
||||
# via
|
||||
# -r typing.txt
|
||||
# types-pygments
|
||||
typing-extensions==4.11.0
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# mypy
|
||||
# pyright
|
||||
# sqlalchemy
|
||||
urllib3==2.2.2
|
||||
# via
|
||||
# -r docs.txt
|
||||
# requests
|
||||
virtualenv==20.26.1
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
werkzeug==3.0.3
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
zipp==3.19.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# importlib-metadata
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
3
requirements/docs.in
Normal file
3
requirements/docs.in
Normal file
@@ -0,0 +1,3 @@
|
||||
pallets-sphinx-themes
|
||||
sphinx
|
||||
sphinxcontrib-log-cabinet
|
||||
63
requirements/docs.txt
Normal file
63
requirements/docs.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile docs.in
|
||||
#
|
||||
alabaster==0.7.13
|
||||
# via sphinx
|
||||
babel==2.14.0
|
||||
# via sphinx
|
||||
certifi==2024.7.4
|
||||
# via requests
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
docutils==0.20.1
|
||||
# via sphinx
|
||||
idna==3.7
|
||||
# via requests
|
||||
imagesize==1.4.1
|
||||
# via sphinx
|
||||
importlib-metadata==7.1.0
|
||||
# via sphinx
|
||||
jinja2==3.1.4
|
||||
# via sphinx
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
packaging==24.0
|
||||
# via
|
||||
# pallets-sphinx-themes
|
||||
# sphinx
|
||||
pallets-sphinx-themes==2.1.3
|
||||
# via -r docs.in
|
||||
pygments==2.18.0
|
||||
# via sphinx
|
||||
pytz==2024.1
|
||||
# via babel
|
||||
requests==2.32.0
|
||||
# via sphinx
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
sphinx==7.1.2
|
||||
# via
|
||||
# -r docs.in
|
||||
# pallets-sphinx-themes
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinxcontrib-applehelp==1.0.4
|
||||
# via sphinx
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r docs.in
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
# via sphinx
|
||||
urllib3==2.2.2
|
||||
# via requests
|
||||
zipp==3.19.1
|
||||
# via importlib-metadata
|
||||
3
requirements/tests.in
Normal file
3
requirements/tests.in
Normal file
@@ -0,0 +1,3 @@
|
||||
pytest
|
||||
flask-sqlalchemy
|
||||
pygments
|
||||
48
requirements/tests.txt
Normal file
48
requirements/tests.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile tests.in
|
||||
#
|
||||
blinker==1.8.1
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
exceptiongroup==1.2.1
|
||||
# via pytest
|
||||
flask==3.0.3
|
||||
# via flask-sqlalchemy
|
||||
flask-sqlalchemy==3.1.1
|
||||
# via -r tests.in
|
||||
greenlet==3.0.3
|
||||
# via sqlalchemy
|
||||
importlib-metadata==7.1.0
|
||||
# via flask
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via flask
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
packaging==24.0
|
||||
# via pytest
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
pygments==2.18.0
|
||||
# via -r tests.in
|
||||
pytest==8.3.3
|
||||
# via -r tests.in
|
||||
sqlalchemy==2.0.29
|
||||
# via flask-sqlalchemy
|
||||
tomli==2.0.1
|
||||
# via pytest
|
||||
typing-extensions==4.11.0
|
||||
# via sqlalchemy
|
||||
werkzeug==3.0.3
|
||||
# via flask
|
||||
zipp==3.19.1
|
||||
# via importlib-metadata
|
||||
5
requirements/typing.in
Normal file
5
requirements/typing.in
Normal file
@@ -0,0 +1,5 @@
|
||||
mypy
|
||||
pyright
|
||||
pytest
|
||||
types-pygments
|
||||
flask-sqlalchemy
|
||||
68
requirements/typing.txt
Normal file
68
requirements/typing.txt
Normal file
@@ -0,0 +1,68 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile typing.in
|
||||
#
|
||||
blinker==1.8.1
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
exceptiongroup==1.2.1
|
||||
# via pytest
|
||||
flask==3.0.3
|
||||
# via flask-sqlalchemy
|
||||
flask-sqlalchemy==3.1.1
|
||||
# via -r typing.in
|
||||
greenlet==3.0.3
|
||||
# via sqlalchemy
|
||||
importlib-metadata==7.1.0
|
||||
# via flask
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via flask
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
mypy==1.11.2
|
||||
# via -r typing.in
|
||||
mypy-extensions==1.0.0
|
||||
# via mypy
|
||||
nodeenv==1.8.0
|
||||
# via pyright
|
||||
packaging==24.0
|
||||
# via pytest
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
pyright==1.1.382.post1
|
||||
# via -r typing.in
|
||||
pytest==8.3.3
|
||||
# via -r typing.in
|
||||
sqlalchemy==2.0.29
|
||||
# via flask-sqlalchemy
|
||||
tomli==2.0.1
|
||||
# via
|
||||
# mypy
|
||||
# pytest
|
||||
types-docutils==0.21.0.20240423
|
||||
# via types-pygments
|
||||
types-pygments==2.18.0.20240506
|
||||
# via -r typing.in
|
||||
types-setuptools==69.5.0.20240423
|
||||
# via types-pygments
|
||||
typing-extensions==4.11.0
|
||||
# via
|
||||
# mypy
|
||||
# pyright
|
||||
# sqlalchemy
|
||||
werkzeug==3.0.3
|
||||
# via flask
|
||||
zipp==3.19.1
|
||||
# via importlib-metadata
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
35
setup.cfg
35
setup.cfg
@@ -1,35 +0,0 @@
|
||||
[metadata]
|
||||
name = Flask-DebugToolbar
|
||||
version = 0.12.0
|
||||
author = Michael van Tellingen
|
||||
author_email = michaelvantellingen@gmail.com
|
||||
maintainer = Matt Good
|
||||
maintainer_email = matt@matt-good.net
|
||||
description = A toolbar overlay for debugging Flask applications.
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
keywords = flask, debug, toolbar
|
||||
url = https://github.com/flask-debugtoolbar/flask-debugtoolbar
|
||||
project_urls =
|
||||
Changelog = https://github.com/jeffwidman/cqlsh#changelog
|
||||
Documentation = https://github.com/flask-debugtoolbar/flask-debugtoolbar/blob/master/CHANGES.rst
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
Environment :: Web Environment
|
||||
Framework :: Flask
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: BSD License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
||||
Topic :: Software Development :: Libraries :: Python Modules
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
package_dir = = flask_debugtoolbar
|
||||
include_package_data = True
|
||||
python_requires = >=2.7
|
||||
# Dependencies are in setup.py for GitHub's dependency graph.
|
||||
|
||||
[options.packages.find]
|
||||
where = flask_debugtoolbar
|
||||
12
setup.py
12
setup.py
@@ -1,12 +0,0 @@
|
||||
from setuptools import setup
|
||||
|
||||
# Metadata goes in setup.cfg. These are here for GitHub's dependency graph.
|
||||
setup(
|
||||
name="Flask-DebugToolbar",
|
||||
install_requires=[
|
||||
'Flask>=0.8',
|
||||
'Blinker',
|
||||
'itsdangerous',
|
||||
'werkzeug',
|
||||
],
|
||||
)
|
||||
359
src/flask_debugtoolbar/__init__.py
Normal file
359
src/flask_debugtoolbar/__init__.py
Normal file
@@ -0,0 +1,359 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as c
|
||||
import os
|
||||
import typing as t
|
||||
import urllib.parse
|
||||
import warnings
|
||||
from contextvars import ContextVar
|
||||
|
||||
import jinja2.ext
|
||||
from flask import Blueprint
|
||||
from flask import current_app
|
||||
from flask import Flask
|
||||
from flask import g
|
||||
from flask import request
|
||||
from flask import send_from_directory
|
||||
from flask import url_for
|
||||
from flask.globals import request_ctx
|
||||
from jinja2 import Environment
|
||||
from jinja2 import PackageLoader
|
||||
from werkzeug import Request
|
||||
from werkzeug import Response
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
from .toolbar import DebugToolbar
|
||||
from .utils import decode_text
|
||||
from .utils import gzip_compress
|
||||
from .utils import gzip_decompress
|
||||
|
||||
module: Blueprint = Blueprint("debugtoolbar", __name__)
|
||||
|
||||
|
||||
def replace_insensitive(string: str, target: str, replacement: str) -> str:
|
||||
"""Similar to string.replace() but is case insensitive
|
||||
Code borrowed from:
|
||||
http://forums.devshed.com/python-programming-11/case-insensitive-string-replace-490921.html
|
||||
"""
|
||||
no_case = string.lower()
|
||||
index = no_case.rfind(target.lower())
|
||||
|
||||
if index >= 0:
|
||||
return string[:index] + replacement + string[index + len(target) :]
|
||||
else: # no results so return the original string
|
||||
return string
|
||||
|
||||
|
||||
def _printable(value: object) -> str:
|
||||
try:
|
||||
return decode_text(repr(value))
|
||||
except Exception as e:
|
||||
return f"<repr({object.__repr__(value)}) raised {type(e).__name__}: {e}>"
|
||||
|
||||
|
||||
class DebugToolbarExtension:
|
||||
_static_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "static"))
|
||||
|
||||
_toolbar_codes = [200, 201, 400, 401, 403, 404, 405, 500, 501, 502, 503, 504]
|
||||
_redirect_codes = [301, 302, 303, 304]
|
||||
|
||||
def __init__(self, app: Flask | None = None) -> None:
|
||||
self.app = app
|
||||
self.toolbar_routes_host: str | None = None
|
||||
|
||||
# Support threads running `flask.copy_current_request_context` without
|
||||
# poping toolbar during `teardown_request`
|
||||
self.debug_toolbars_var: ContextVar[dict[Request, DebugToolbar]] = ContextVar(
|
||||
"debug_toolbars"
|
||||
)
|
||||
jinja_extensions = [jinja2.ext.i18n]
|
||||
|
||||
# Jinja2<3
|
||||
if hasattr(jinja2.ext, "with_"):
|
||||
jinja_extensions.append(jinja2.ext.with_) # pyright: ignore
|
||||
|
||||
# Configure jinja for the internal templates and add url rules
|
||||
# for static data
|
||||
self.jinja_env: Environment = Environment(
|
||||
autoescape=True,
|
||||
extensions=jinja_extensions,
|
||||
loader=PackageLoader(__name__, "templates"),
|
||||
)
|
||||
self.jinja_env.filters["urlencode"] = urllib.parse.quote_plus
|
||||
self.jinja_env.filters["printable"] = _printable
|
||||
self.jinja_env.globals["url_for"] = url_for
|
||||
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app: Flask) -> None:
|
||||
for k, v in self._default_config(app).items():
|
||||
app.config.setdefault(k, v)
|
||||
|
||||
if not app.config["DEBUG_TB_ENABLED"]:
|
||||
return
|
||||
|
||||
if not app.config.get("SECRET_KEY"):
|
||||
raise RuntimeError(
|
||||
"The Flask-DebugToolbar requires the 'SECRET_KEY' config "
|
||||
"var to be set"
|
||||
)
|
||||
|
||||
self._validate_and_configure_toolbar_routes_host(app)
|
||||
|
||||
DebugToolbar.load_panels(app)
|
||||
|
||||
app.before_request(self.process_request)
|
||||
app.after_request(self.process_response)
|
||||
app.teardown_request(self.teardown_request)
|
||||
|
||||
# Monkey-patch the Flask.dispatch_request method
|
||||
app.dispatch_request = self.dispatch_request # type: ignore[method-assign]
|
||||
|
||||
app.add_url_rule(
|
||||
"/_debug_toolbar/static/<path:filename>",
|
||||
"_debug_toolbar.static",
|
||||
self.send_static_file,
|
||||
host=self.toolbar_routes_host,
|
||||
)
|
||||
|
||||
app.register_blueprint(module, url_prefix="/_debug_toolbar/views")
|
||||
|
||||
def _default_config(self, app: Flask) -> dict[str, t.Any]:
|
||||
return {
|
||||
"DEBUG_TB_ENABLED": app.debug,
|
||||
"DEBUG_TB_HOSTS": (),
|
||||
"DEBUG_TB_ROUTES_HOST": None,
|
||||
"DEBUG_TB_INTERCEPT_REDIRECTS": True,
|
||||
"DEBUG_TB_PANELS": (
|
||||
"flask_debugtoolbar.panels.versions.VersionDebugPanel",
|
||||
"flask_debugtoolbar.panels.timer.TimerDebugPanel",
|
||||
"flask_debugtoolbar.panels.headers.HeaderDebugPanel",
|
||||
"flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel",
|
||||
"flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel",
|
||||
"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",
|
||||
"flask_debugtoolbar.panels.g.GDebugPanel",
|
||||
),
|
||||
"SQLALCHEMY_RECORD_QUERIES": app.debug,
|
||||
}
|
||||
|
||||
def _validate_and_configure_toolbar_routes_host(self, app: Flask) -> None:
|
||||
toolbar_routes_host = app.config["DEBUG_TB_ROUTES_HOST"]
|
||||
if app.url_map.host_matching and not toolbar_routes_host:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Flask-DebugToolbar requires DEBUG_TB_ROUTES_HOST to be set if Flask "
|
||||
"is running in `host_matching` mode. Static assets for the toolbar "
|
||||
"will not be served correctly unless this is set.",
|
||||
stacklevel=1,
|
||||
)
|
||||
|
||||
if toolbar_routes_host:
|
||||
if not app.url_map.host_matching:
|
||||
raise ValueError(
|
||||
"`DEBUG_TB_ROUTES_HOST` should only be set if your Flask app is "
|
||||
"using `host_matching`."
|
||||
)
|
||||
|
||||
if toolbar_routes_host.strip() == "*":
|
||||
toolbar_routes_host = "<toolbar_routes_host>"
|
||||
elif "<" in toolbar_routes_host and ">" in toolbar_routes_host:
|
||||
raise ValueError(
|
||||
"`DEBUG_TB_ROUTES_HOST` must either be a host name with no "
|
||||
"variables, to serve all Flask-DebugToolbar assets from a single "
|
||||
"host, or `*` to match the current request's host."
|
||||
)
|
||||
|
||||
# Automatically inject `toolbar_routes_host` into `url_for` calls for
|
||||
# the toolbar's `send_static_file` method.
|
||||
@app.url_defaults
|
||||
def inject_toolbar_routes_host_if_required(
|
||||
endpoint: str, values: dict[str, t.Any]
|
||||
) -> None:
|
||||
if app.url_map.is_endpoint_expecting(endpoint, "toolbar_routes_host"):
|
||||
values.setdefault("toolbar_routes_host", request.host)
|
||||
|
||||
# Automatically strip `toolbar_routes_host` from the endpoint values so
|
||||
# that the `send_static_host` method doesn't receive that parameter,
|
||||
# as it's not actually required internally.
|
||||
@app.url_value_preprocessor
|
||||
def strip_toolbar_routes_host_from_static_endpoint(
|
||||
endpoint: str | None, values: dict[str, t.Any] | None
|
||||
) -> None:
|
||||
if (
|
||||
endpoint
|
||||
and values
|
||||
and app.url_map.is_endpoint_expecting(
|
||||
endpoint, "toolbar_routes_host"
|
||||
)
|
||||
):
|
||||
values.pop("toolbar_routes_host", None)
|
||||
|
||||
self.toolbar_routes_host = toolbar_routes_host
|
||||
|
||||
def dispatch_request(self) -> t.Any:
|
||||
"""Modified version of ``Flask.dispatch_request`` to call
|
||||
:meth:`process_view`.
|
||||
"""
|
||||
# self references this extension, use current_app to call app methods.
|
||||
app = current_app._get_current_object() # type: ignore[attr-defined]
|
||||
req = request_ctx.request
|
||||
|
||||
if req.routing_exception is not None:
|
||||
app.raise_routing_exception(req)
|
||||
|
||||
rule: Rule = req.url_rule # type: ignore[assignment]
|
||||
|
||||
if (
|
||||
getattr(rule, "provide_automatic_options", False)
|
||||
and req.method == "OPTIONS"
|
||||
):
|
||||
return app.make_default_options_response()
|
||||
|
||||
view_func = app.view_functions[rule.endpoint]
|
||||
view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]
|
||||
# allow each toolbar to process the view and args
|
||||
view_func = self.process_view(app, view_func, view_args)
|
||||
return view_func(**view_args)
|
||||
|
||||
def _show_toolbar(self) -> bool:
|
||||
"""Return a boolean to indicate if we need to show the toolbar."""
|
||||
if request.blueprint == "debugtoolbar":
|
||||
return False
|
||||
|
||||
hosts = current_app.config["DEBUG_TB_HOSTS"]
|
||||
|
||||
if hosts and request.remote_addr not in hosts:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def send_static_file(self, filename: str) -> Response:
|
||||
"""Send a static file from the flask-debugtoolbar static directory."""
|
||||
return send_from_directory(self._static_dir, filename)
|
||||
|
||||
def process_request(self) -> None:
|
||||
g.debug_toolbar = self
|
||||
|
||||
if not self._show_toolbar():
|
||||
return
|
||||
|
||||
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||
self.debug_toolbars_var.set({})
|
||||
self.debug_toolbars_var.get()[real_request] = DebugToolbar(
|
||||
real_request, self.jinja_env
|
||||
)
|
||||
|
||||
for panel in self.debug_toolbars_var.get()[real_request].panels:
|
||||
panel.process_request(real_request)
|
||||
|
||||
def process_view(
|
||||
self,
|
||||
app: Flask,
|
||||
view_func: c.Callable[..., t.Any],
|
||||
view_kwargs: dict[str, t.Any],
|
||||
) -> c.Callable[..., t.Any]:
|
||||
"""This method is called just before the flask view is called.
|
||||
This is done by the dispatch_request method.
|
||||
"""
|
||||
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||
|
||||
try:
|
||||
toolbar = self.debug_toolbars_var.get({})[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: Response) -> Response:
|
||||
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||
|
||||
if real_request not in self.debug_toolbars_var.get({}):
|
||||
return response
|
||||
|
||||
# Intercept http redirect codes and display an html page with a
|
||||
# link to the target.
|
||||
if current_app.config["DEBUG_TB_INTERCEPT_REDIRECTS"]:
|
||||
if response.status_code in self._redirect_codes:
|
||||
redirect_to = response.location
|
||||
redirect_code = response.status_code
|
||||
|
||||
if redirect_to:
|
||||
content = self.render(
|
||||
"redirect.html",
|
||||
{"redirect_to": redirect_to, "redirect_code": redirect_code},
|
||||
)
|
||||
response.content_length = len(content)
|
||||
del response.location
|
||||
response.response = [content]
|
||||
response.status_code = 200
|
||||
|
||||
# If the http response code is an allowed code then we process to add the
|
||||
# toolbar to the returned html response.
|
||||
if not (
|
||||
response.status_code in self._toolbar_codes
|
||||
and response.is_sequence
|
||||
and response.headers["content-type"].startswith("text/html")
|
||||
):
|
||||
return response
|
||||
|
||||
content_encoding = response.headers.get("Content-Encoding")
|
||||
|
||||
if content_encoding and "gzip" in content_encoding:
|
||||
response_html = gzip_decompress(response.data).decode()
|
||||
else:
|
||||
response_html = response.get_data(as_text=True)
|
||||
|
||||
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.",
|
||||
stacklevel=1,
|
||||
)
|
||||
return response
|
||||
|
||||
toolbar = self.debug_toolbars_var.get()[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_bytes = content.encode("utf-8")
|
||||
|
||||
if content_encoding and "gzip" in content_encoding:
|
||||
content_bytes = gzip_compress(content_bytes)
|
||||
|
||||
response.response = [content_bytes]
|
||||
response.content_length = len(content_bytes)
|
||||
|
||||
return response
|
||||
|
||||
def teardown_request(self, exc: BaseException | None) -> None:
|
||||
# debug_toolbars_var won't be set under `flask.copy_current_request_context`
|
||||
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||
self.debug_toolbars_var.get({}).pop(real_request, None)
|
||||
|
||||
def render(self, template_name: str, context: dict[str, t.Any]) -> str:
|
||||
template = self.jinja_env.get_template(template_name)
|
||||
return template.render(**context)
|
||||
99
src/flask_debugtoolbar/panels/__init__.py
Normal file
99
src/flask_debugtoolbar/panels/__init__.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as c
|
||||
import typing as t
|
||||
|
||||
from flask import Flask
|
||||
from jinja2 import Environment
|
||||
from werkzeug import Request
|
||||
from werkzeug import Response
|
||||
|
||||
|
||||
class DebugPanel:
|
||||
"""Base class for debug panels."""
|
||||
|
||||
name: str
|
||||
|
||||
# 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
|
||||
|
||||
# We'll maintain a local context instance so we can expose our template
|
||||
# context variables to panels which need them:
|
||||
context: dict[str, t.Any] = {}
|
||||
|
||||
# Panel methods
|
||||
def __init__(
|
||||
self, jinja_env: Environment, context: dict[str, t.Any] | None = None
|
||||
) -> None:
|
||||
if context is not None:
|
||||
self.context.update(context)
|
||||
|
||||
self.jinja_env = jinja_env
|
||||
# If the client enabled the panel
|
||||
self.is_active = False
|
||||
|
||||
@classmethod
|
||||
def init_app(cls, app: Flask) -> None:
|
||||
"""Method that can be overridden by child classes.
|
||||
Can be used for setting up additional URL-rules/routes.
|
||||
|
||||
Example::
|
||||
|
||||
class UMLDiagramPanel(DebugPanel):
|
||||
|
||||
@classmethod
|
||||
def init_app(cls, app):
|
||||
app.add_url_rule(
|
||||
'/_flask_debugtoolbar_umldiagram/<path:filename>',
|
||||
'_flask_debugtoolbar_umldiagram.serve_generated_image',
|
||||
cls.serve_generated_image
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def serve_generated_image(cls, app):
|
||||
return Response(...)
|
||||
"""
|
||||
pass
|
||||
|
||||
def render(self, template_name: str, context: dict[str, t.Any]) -> str:
|
||||
template = self.jinja_env.get_template(template_name)
|
||||
return template.render(**context)
|
||||
|
||||
def dom_id(self) -> str:
|
||||
return f"flDebug{self.name.replace(' ', '')}Panel"
|
||||
|
||||
def nav_title(self) -> str:
|
||||
"""Title showing in toolbar"""
|
||||
raise NotImplementedError
|
||||
|
||||
def nav_subtitle(self) -> str:
|
||||
"""Subtitle showing until title in toolbar"""
|
||||
return ""
|
||||
|
||||
def title(self) -> str:
|
||||
"""Title showing in panel"""
|
||||
raise NotImplementedError
|
||||
|
||||
def url(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def content(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
# Standard middleware methods
|
||||
def process_request(self, request: Request) -> None:
|
||||
pass
|
||||
|
||||
def process_view(
|
||||
self,
|
||||
request: Request,
|
||||
view_func: c.Callable[..., t.Any],
|
||||
view_kwargs: dict[str, t.Any],
|
||||
) -> c.Callable[..., t.Any] | None:
|
||||
pass
|
||||
|
||||
def process_response(self, request: Request, response: Response) -> None:
|
||||
pass
|
||||
30
src/flask_debugtoolbar/panels/config_vars.py
Normal file
30
src/flask_debugtoolbar/panels/config_vars.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class ConfigVarsDebugPanel(DebugPanel):
|
||||
"""A panel to display all variables from Flask configuration."""
|
||||
|
||||
name = "ConfigVars"
|
||||
has_content = True
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "Config"
|
||||
|
||||
def title(self) -> str:
|
||||
return "Config"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self) -> str:
|
||||
context = self.context.copy()
|
||||
context.update(
|
||||
{
|
||||
"config": current_app.config,
|
||||
}
|
||||
)
|
||||
return self.render("panels/config_vars.html", context)
|
||||
26
src/flask_debugtoolbar/panels/g.py
Normal file
26
src/flask_debugtoolbar/panels/g.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import g
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class GDebugPanel(DebugPanel):
|
||||
"""A panel to display ``flask.g`` content."""
|
||||
|
||||
name = "g"
|
||||
has_content = True
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "flask.g"
|
||||
|
||||
def title(self) -> str:
|
||||
return "flask.g content"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self) -> str:
|
||||
context = self.context.copy()
|
||||
context.update({"g_content": g.__dict__})
|
||||
return self.render("panels/g.html", context)
|
||||
56
src/flask_debugtoolbar/panels/headers.py
Normal file
56
src/flask_debugtoolbar/panels/headers.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
from werkzeug import Request
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class HeaderDebugPanel(DebugPanel):
|
||||
"""A panel to display HTTP headers."""
|
||||
|
||||
name = "Header"
|
||||
has_content = True
|
||||
# List of headers we want to display
|
||||
header_filter: tuple[str, ...] = (
|
||||
"CONTENT_TYPE",
|
||||
"HTTP_ACCEPT",
|
||||
"HTTP_ACCEPT_CHARSET",
|
||||
"HTTP_ACCEPT_ENCODING",
|
||||
"HTTP_ACCEPT_LANGUAGE",
|
||||
"HTTP_CACHE_CONTROL",
|
||||
"HTTP_CONNECTION",
|
||||
"HTTP_HOST",
|
||||
"HTTP_KEEP_ALIVE",
|
||||
"HTTP_REFERER",
|
||||
"HTTP_USER_AGENT",
|
||||
"QUERY_STRING",
|
||||
"REMOTE_ADDR",
|
||||
"REMOTE_HOST",
|
||||
"REQUEST_METHOD",
|
||||
"SCRIPT_NAME",
|
||||
"SERVER_NAME",
|
||||
"SERVER_PORT",
|
||||
"SERVER_PROTOCOL",
|
||||
"SERVER_SOFTWARE",
|
||||
)
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "HTTP Headers"
|
||||
|
||||
def title(self) -> str:
|
||||
return "HTTP Headers"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def process_request(self, request: Request) -> None:
|
||||
self.headers: dict[str, t.Any] = {
|
||||
k: request.environ[k] for k in self.header_filter if k in request.environ
|
||||
}
|
||||
|
||||
def content(self) -> str:
|
||||
context = self.context.copy()
|
||||
context.update({"headers": self.headers})
|
||||
return self.render("panels/headers.html", context)
|
||||
115
src/flask_debugtoolbar/panels/logger.py
Normal file
115
src/flask_debugtoolbar/panels/logger.py
Normal file
@@ -0,0 +1,115 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from werkzeug import Request
|
||||
|
||||
from ..utils import format_fname
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class ThreadTrackingHandler(logging.Handler):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
# a dictionary that maps threads to log records
|
||||
self.records: dict[threading.Thread, list[logging.LogRecord]] = {}
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
self.get_records().append(record)
|
||||
|
||||
def get_records(
|
||||
self, thread: threading.Thread | None = None
|
||||
) -> list[logging.LogRecord]:
|
||||
"""
|
||||
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.current_thread()
|
||||
|
||||
if thread not in self.records:
|
||||
self.records[thread] = []
|
||||
|
||||
return self.records[thread]
|
||||
|
||||
def clear_records(self, thread: threading.Thread | None = None) -> None:
|
||||
if thread is None:
|
||||
thread = threading.current_thread()
|
||||
|
||||
if thread in self.records:
|
||||
del self.records[thread]
|
||||
|
||||
|
||||
handler: ThreadTrackingHandler = None # type: ignore[assignment]
|
||||
_init_lock = threading.Lock()
|
||||
|
||||
|
||||
def _init_once() -> None:
|
||||
global handler
|
||||
|
||||
if handler is not None:
|
||||
return
|
||||
|
||||
with _init_lock:
|
||||
if handler is not None:
|
||||
return
|
||||
|
||||
# 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.
|
||||
from werkzeug._internal import _log
|
||||
|
||||
_log("debug", "Initializing Flask-DebugToolbar log handler")
|
||||
handler = ThreadTrackingHandler()
|
||||
logging.root.addHandler(handler)
|
||||
|
||||
|
||||
class LoggingPanel(DebugPanel):
|
||||
name = "Logging"
|
||||
has_content = True
|
||||
|
||||
def process_request(self, request: Request) -> None:
|
||||
_init_once()
|
||||
handler.clear_records()
|
||||
|
||||
def get_and_delete(self) -> list[logging.LogRecord]:
|
||||
records = handler.get_records()
|
||||
handler.clear_records()
|
||||
return records
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "Logging"
|
||||
|
||||
def nav_subtitle(self) -> str:
|
||||
num_records = len(handler.get_records())
|
||||
plural = "message" if num_records == 1 else "messages"
|
||||
return f"{num_records} {plural}"
|
||||
|
||||
def title(self) -> str:
|
||||
return "Log Messages"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self) -> str:
|
||||
records = []
|
||||
|
||||
for record in self.get_and_delete():
|
||||
records.append(
|
||||
{
|
||||
"message": record.getMessage(),
|
||||
"time": datetime.datetime.fromtimestamp(record.created),
|
||||
"level": record.levelname,
|
||||
"file": format_fname(record.pathname),
|
||||
"file_long": record.pathname,
|
||||
"line": record.lineno,
|
||||
}
|
||||
)
|
||||
|
||||
context = self.context.copy()
|
||||
context.update({"records": records})
|
||||
return self.render("panels/logger.html", context)
|
||||
156
src/flask_debugtoolbar/panels/profiler.py
Normal file
156
src/flask_debugtoolbar/panels/profiler.py
Normal file
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as c
|
||||
import functools
|
||||
import pstats
|
||||
import typing as t
|
||||
|
||||
from flask import current_app
|
||||
from jinja2 import Environment
|
||||
from werkzeug import Request
|
||||
from werkzeug import Response
|
||||
|
||||
from ..utils import format_fname
|
||||
from . import DebugPanel
|
||||
|
||||
try:
|
||||
import cProfile as profile
|
||||
except ImportError:
|
||||
import profile # type: ignore[no-redef]
|
||||
|
||||
|
||||
class ProfilerDebugPanel(DebugPanel):
|
||||
"""Panel that displays the time a response took with cProfile output."""
|
||||
|
||||
name = "Profiler"
|
||||
user_activate = True
|
||||
|
||||
is_active: bool = False
|
||||
dump_filename: str | None = None
|
||||
profiler: profile.Profile
|
||||
stats: pstats.Stats | None = None
|
||||
function_calls: list[dict[str, t.Any]]
|
||||
|
||||
def __init__(
|
||||
self, jinja_env: Environment, context: dict[str, t.Any] | None = None
|
||||
) -> None:
|
||||
super().__init__(jinja_env, context=context)
|
||||
|
||||
if current_app.config.get("DEBUG_TB_PROFILER_ENABLED"):
|
||||
self.is_active = True
|
||||
self.dump_filename = current_app.config.get(
|
||||
"DEBUG_TB_PROFILER_DUMP_FILENAME"
|
||||
)
|
||||
|
||||
@property
|
||||
def has_content(self) -> bool: # type: ignore[override]
|
||||
return bool(self.profiler)
|
||||
|
||||
def process_request(self, request: Request) -> None:
|
||||
if not self.is_active:
|
||||
return
|
||||
|
||||
self.profiler = profile.Profile() # pyright: ignore
|
||||
self.stats = None
|
||||
|
||||
def process_view(
|
||||
self,
|
||||
request: Request,
|
||||
view_func: c.Callable[..., t.Any],
|
||||
view_kwargs: dict[str, t.Any],
|
||||
) -> c.Callable[..., t.Any] | None:
|
||||
if self.is_active:
|
||||
func = functools.partial(self.profiler.runcall, view_func)
|
||||
functools.update_wrapper(func, view_func)
|
||||
return func
|
||||
|
||||
return None
|
||||
|
||||
def process_response(self, request: Request, response: Response) -> None:
|
||||
if not self.is_active:
|
||||
return
|
||||
|
||||
if self.profiler is not None:
|
||||
self.profiler.disable() # pyright: ignore
|
||||
|
||||
try:
|
||||
stats = pstats.Stats(self.profiler)
|
||||
except TypeError:
|
||||
self.is_active = False
|
||||
return
|
||||
|
||||
function_calls: list[dict[str, t.Any]] = []
|
||||
|
||||
for func in stats.sort_stats(1).fcn_list: # type: ignore[attr-defined]
|
||||
current: dict[str, t.Any] = {}
|
||||
info = stats.stats[func] # type: ignore[attr-defined]
|
||||
|
||||
# Number of calls
|
||||
if info[0] != info[1]:
|
||||
current["ncalls"] = f"{info[1]}/{info[0]}"
|
||||
else:
|
||||
current["ncalls"] = info[1]
|
||||
|
||||
# Total time
|
||||
current["tottime"] = info[2] * 1000
|
||||
|
||||
# Quotient of total time divided by number of calls
|
||||
if info[1]:
|
||||
current["percall"] = info[2] * 1000 / info[1]
|
||||
else:
|
||||
current["percall"] = 0
|
||||
|
||||
# Cumulative time
|
||||
current["cumtime"] = info[3] * 1000
|
||||
|
||||
# Quotient of the cumulative time divided by the number of
|
||||
# primitive calls.
|
||||
if info[0]:
|
||||
current["percall_cum"] = info[3] * 1000 / info[0]
|
||||
else:
|
||||
current["percall_cum"] = 0
|
||||
|
||||
# Filename
|
||||
filename = pstats.func_std_string(func) # type: ignore[attr-defined]
|
||||
current["filename_long"] = filename
|
||||
current["filename"] = format_fname(filename)
|
||||
function_calls.append(current)
|
||||
|
||||
self.stats = stats
|
||||
self.function_calls = function_calls
|
||||
|
||||
if self.dump_filename:
|
||||
if callable(self.dump_filename):
|
||||
filename = self.dump_filename()
|
||||
else:
|
||||
filename = self.dump_filename
|
||||
|
||||
self.profiler.dump_stats(filename)
|
||||
|
||||
def title(self) -> str:
|
||||
if not self.is_active:
|
||||
return "Profiler not active"
|
||||
|
||||
return f"View: {float(self.stats.total_tt) * 1000:.2f}ms" # type: ignore[union-attr]
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "Profiler"
|
||||
|
||||
def nav_subtitle(self) -> str:
|
||||
if not self.is_active:
|
||||
return "in-active"
|
||||
|
||||
return f"View: {float(self.stats.total_tt) * 1000:.2f}ms" # type: ignore[union-attr]
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self) -> str:
|
||||
if not self.is_active:
|
||||
return "The profiler is not activated, activate it to use it"
|
||||
|
||||
context = {
|
||||
"stats": self.stats,
|
||||
"function_calls": self.function_calls,
|
||||
}
|
||||
return self.render("panels/profiler.html", context)
|
||||
59
src/flask_debugtoolbar/panels/request_vars.py
Normal file
59
src/flask_debugtoolbar/panels/request_vars.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as c
|
||||
import typing as t
|
||||
|
||||
from flask import session
|
||||
from werkzeug import Request
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class RequestVarsDebugPanel(DebugPanel):
|
||||
"""A panel to display request variables (POST/GET, session, cookies)."""
|
||||
|
||||
name = "RequestVars"
|
||||
has_content = True
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "Request Vars"
|
||||
|
||||
def title(self) -> str:
|
||||
return "Request Vars"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def process_request(self, request: Request) -> None:
|
||||
self.request = request
|
||||
self.session = session
|
||||
self.view_func: c.Callable[..., t.Any] | None = None
|
||||
self.view_kwargs: dict[str, t.Any] = {}
|
||||
|
||||
def process_view(
|
||||
self,
|
||||
request: Request,
|
||||
view_func: c.Callable[..., t.Any],
|
||||
view_kwargs: dict[str, t.Any],
|
||||
) -> None:
|
||||
self.view_func = view_func
|
||||
self.view_kwargs = view_kwargs
|
||||
|
||||
def content(self) -> str:
|
||||
context = self.context.copy()
|
||||
context.update(
|
||||
{
|
||||
"get": self.request.args.lists(),
|
||||
"post": self.request.form.lists(),
|
||||
"cookies": self.request.cookies.items(),
|
||||
"view_func": (
|
||||
f"{self.view_func.__module__}.{self.view_func.__name__}"
|
||||
if self.view_func
|
||||
else "[unknown]"
|
||||
),
|
||||
"view_kwargs": self.view_kwargs or {},
|
||||
"session": self.session.items(),
|
||||
}
|
||||
)
|
||||
|
||||
return self.render("panels/request_vars.html", context)
|
||||
44
src/flask_debugtoolbar/panels/route_list.py
Normal file
44
src/flask_debugtoolbar/panels/route_list.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import current_app
|
||||
from werkzeug import Request
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class RouteListDebugPanel(DebugPanel):
|
||||
"""Panel that displays the URL routing rules."""
|
||||
|
||||
name = "RouteList"
|
||||
has_content = True
|
||||
routes: list[Rule] = []
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "Route List"
|
||||
|
||||
def title(self) -> str:
|
||||
return "Route List"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def nav_subtitle(self) -> str:
|
||||
count = len(self.routes)
|
||||
plural = "route" if count == 1 else "routes"
|
||||
return f"{count} {plural}"
|
||||
|
||||
def process_request(self, request: Request) -> None:
|
||||
self.routes = [
|
||||
rule
|
||||
for rule in current_app.url_map.iter_rules()
|
||||
if not rule.rule.startswith("/_debug_toolbar")
|
||||
]
|
||||
|
||||
def content(self) -> str:
|
||||
return self.render(
|
||||
"panels/route_list.html",
|
||||
{
|
||||
"routes": self.routes,
|
||||
},
|
||||
)
|
||||
186
src/flask_debugtoolbar/panels/sqlalchemy.py
Normal file
186
src/flask_debugtoolbar/panels/sqlalchemy.py
Normal file
@@ -0,0 +1,186 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing as t
|
||||
|
||||
import itsdangerous
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import request
|
||||
|
||||
from .. import module
|
||||
from ..utils import format_fname
|
||||
from ..utils import format_sql
|
||||
from . import DebugPanel
|
||||
|
||||
try:
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
except ImportError:
|
||||
sqlalchemy_available: bool = False
|
||||
get_recorded_queries = SQLAlchemy = None # type: ignore[misc, assignment]
|
||||
debug_enables_record_queries: bool = False
|
||||
else:
|
||||
try:
|
||||
from flask_sqlalchemy.record_queries import ( # type: ignore[assignment]
|
||||
get_recorded_queries,
|
||||
)
|
||||
|
||||
debug_enables_record_queries = False
|
||||
except ImportError:
|
||||
# For flask_sqlalchemy < 3.0.0
|
||||
from flask_sqlalchemy import ( # type: ignore[no-redef]
|
||||
get_debug_queries as get_recorded_queries,
|
||||
)
|
||||
|
||||
# flask_sqlalchemy < 3.0.0 automatically enabled
|
||||
# SQLALCHEMY_RECORD_QUERIES in debug or test mode
|
||||
debug_enables_record_queries = True
|
||||
location_property: str = "context"
|
||||
else:
|
||||
location_property = "location"
|
||||
|
||||
sqlalchemy_available = True
|
||||
|
||||
|
||||
def query_signer() -> itsdangerous.URLSafeSerializer:
|
||||
return itsdangerous.URLSafeSerializer(
|
||||
current_app.config["SECRET_KEY"], salt="fdt-sql-query"
|
||||
)
|
||||
|
||||
|
||||
def is_select(statement: str | bytes) -> bool:
|
||||
statement = statement.lower().strip()
|
||||
|
||||
if isinstance(statement, bytes):
|
||||
return statement.startswith(b"select")
|
||||
|
||||
return statement.startswith("select") # pyright: ignore
|
||||
|
||||
|
||||
def dump_query(statement: str, params: t.Any) -> str | None:
|
||||
if not params or not is_select(statement):
|
||||
return None
|
||||
|
||||
try:
|
||||
return query_signer().dumps([statement, params])
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
|
||||
def load_query(data: str) -> tuple[str, t.Any]:
|
||||
try:
|
||||
statement, params = query_signer().loads(data)
|
||||
except (itsdangerous.BadSignature, TypeError):
|
||||
abort(406)
|
||||
|
||||
# Make sure it is a select statement
|
||||
if not is_select(statement):
|
||||
abort(406)
|
||||
|
||||
return statement, params
|
||||
|
||||
|
||||
def extension_used() -> bool:
|
||||
return "sqlalchemy" in current_app.extensions
|
||||
|
||||
|
||||
def recording_enabled() -> bool:
|
||||
return (
|
||||
debug_enables_record_queries and current_app.debug
|
||||
) or current_app.config.get("SQLALCHEMY_RECORD_QUERIES", False)
|
||||
|
||||
|
||||
def is_available() -> bool:
|
||||
return sqlalchemy_available and extension_used() and recording_enabled()
|
||||
|
||||
|
||||
def get_queries() -> list[t.Any]:
|
||||
if get_recorded_queries:
|
||||
return get_recorded_queries()
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
class SQLAlchemyDebugPanel(DebugPanel):
|
||||
"""Panel that displays the time a response took in milliseconds."""
|
||||
|
||||
name = "SQLAlchemy"
|
||||
|
||||
@property
|
||||
def has_content(self) -> bool: # type: ignore[override]
|
||||
return bool(get_queries()) or not is_available()
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "SQLAlchemy"
|
||||
|
||||
def nav_subtitle(self) -> str:
|
||||
count = len(get_queries())
|
||||
|
||||
if not count and not is_available():
|
||||
return "Unavailable"
|
||||
|
||||
plural = "query" if count == 1 else "queries"
|
||||
return f"{count} {plural}"
|
||||
|
||||
def title(self) -> str:
|
||||
return "SQLAlchemy queries"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self) -> str:
|
||||
queries = get_queries()
|
||||
|
||||
if not queries and not is_available():
|
||||
return self.render(
|
||||
"panels/sqlalchemy_error.html",
|
||||
{
|
||||
"sqlalchemy_available": sqlalchemy_available,
|
||||
"extension_used": extension_used(),
|
||||
"recording_enabled": recording_enabled(),
|
||||
},
|
||||
)
|
||||
|
||||
data = []
|
||||
|
||||
for query in queries:
|
||||
data.append(
|
||||
{
|
||||
"duration": query.duration,
|
||||
"sql": format_sql(query.statement, query.parameters),
|
||||
"signed_query": dump_query(query.statement, query.parameters),
|
||||
"location_long": getattr(query, location_property),
|
||||
"location": format_fname(getattr(query, location_property)),
|
||||
}
|
||||
)
|
||||
|
||||
return self.render("panels/sqlalchemy.html", {"queries": data})
|
||||
|
||||
|
||||
# Panel views
|
||||
|
||||
|
||||
@module.route("/sqlalchemy/sql_select", methods=["GET", "POST"])
|
||||
@module.route(
|
||||
"/sqlalchemy/sql_explain", methods=["GET", "POST"], defaults=dict(explain=True)
|
||||
)
|
||||
def sql_select(explain: bool = False) -> str:
|
||||
statement, params = load_query(request.args["query"])
|
||||
engine = current_app.extensions["sqlalchemy"].engine
|
||||
|
||||
if explain:
|
||||
if engine.driver == "pysqlite":
|
||||
statement = f"EXPLAIN QUERY PLAN\n{statement}"
|
||||
else:
|
||||
statement = f"EXPLAIN\n{statement}"
|
||||
|
||||
result = engine.execute(statement, params)
|
||||
return g.debug_toolbar.render( # type: ignore[no-any-return]
|
||||
"panels/sqlalchemy_select.html",
|
||||
{
|
||||
"result": result.fetchall(),
|
||||
"headers": result.keys(),
|
||||
"sql": format_sql(statement, params),
|
||||
"duration": float(request.args["duration"]),
|
||||
},
|
||||
)
|
||||
149
src/flask_debugtoolbar/panels/template.py
Normal file
149
src/flask_debugtoolbar/panels/template.py
Normal file
@@ -0,0 +1,149 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import typing as t
|
||||
import uuid
|
||||
from collections import deque
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import request
|
||||
from flask import Response
|
||||
from flask import template_rendered
|
||||
from flask import url_for
|
||||
from jinja2 import Template
|
||||
|
||||
from .. import module
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class TemplateDebugPanel(DebugPanel):
|
||||
"""Panel that displays the time a response took in milliseconds."""
|
||||
|
||||
name = "Template"
|
||||
has_content = True
|
||||
|
||||
# save the context for the 5 most recent requests
|
||||
template_cache: deque[tuple[str, list[dict[str, t.Any]]]] = deque(maxlen=5)
|
||||
|
||||
@classmethod
|
||||
def get_cache_for_key(cls, key: str) -> list[dict[str, t.Any]]:
|
||||
for cache_key, value in cls.template_cache:
|
||||
if key == cache_key:
|
||||
return value
|
||||
|
||||
raise KeyError(key)
|
||||
|
||||
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.key: str = str(uuid.uuid4())
|
||||
self.templates: list[dict[str, t.Any]] = []
|
||||
template_rendered.connect(self._store_template_info)
|
||||
|
||||
def _store_template_info(self, sender: t.Any, **kwargs: t.Any) -> None:
|
||||
# only record in the cache if the editor is enabled and there is
|
||||
# actually a template for this request
|
||||
if not self.templates and is_editor_enabled():
|
||||
self.template_cache.append((self.key, self.templates))
|
||||
|
||||
self.templates.append(kwargs)
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "Templates"
|
||||
|
||||
def nav_subtitle(self) -> str:
|
||||
return f"{len(self.templates)} rendered"
|
||||
|
||||
def title(self) -> str:
|
||||
return "Templates"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self) -> str:
|
||||
return self.render(
|
||||
"panels/template.html",
|
||||
{
|
||||
"key": self.key,
|
||||
"templates": self.templates,
|
||||
"editable": is_editor_enabled(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def is_editor_enabled() -> bool:
|
||||
return current_app.config.get("DEBUG_TB_TEMPLATE_EDITOR_ENABLED", False) # type: ignore
|
||||
|
||||
|
||||
def require_enabled() -> None:
|
||||
if not is_editor_enabled():
|
||||
abort(403)
|
||||
|
||||
|
||||
def _get_source(template: Template) -> str:
|
||||
if template.filename is None:
|
||||
return ""
|
||||
|
||||
with open(template.filename, "rb") as fp:
|
||||
source = fp.read()
|
||||
|
||||
return source.decode(_template_encoding())
|
||||
|
||||
|
||||
def _template_encoding() -> str:
|
||||
return getattr(current_app.jinja_loader, "encoding", "utf-8")
|
||||
|
||||
|
||||
@module.route("/template/<key>")
|
||||
def template_editor(key: str) -> str:
|
||||
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)]
|
||||
return g.debug_toolbar.render( # type: ignore[no-any-return]
|
||||
"panels/template_editor.html",
|
||||
{
|
||||
"static_path": url_for("_debug_toolbar.static", filename=""),
|
||||
"request": request,
|
||||
"templates": [
|
||||
{"name": t.name, "source": _get_source(t)} for t in templates
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@module.route("/template/<key>/save", methods=["POST"])
|
||||
def save_template(key: str) -> str:
|
||||
require_enabled()
|
||||
template = TemplateDebugPanel.get_cache_for_key(key)[0]["template"]
|
||||
content = request.form["content"].encode(_template_encoding())
|
||||
|
||||
with open(template.filename, "wb") as fp:
|
||||
fp.write(content)
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@module.route("/template/<key>", methods=["POST"])
|
||||
def template_preview(key: str) -> str | Response:
|
||||
require_enabled()
|
||||
context = TemplateDebugPanel.get_cache_for_key(key)[0]["context"]
|
||||
content = request.form["content"]
|
||||
env = current_app.jinja_env.overlay(autoescape=True)
|
||||
|
||||
try:
|
||||
template = env.from_string(content)
|
||||
return template.render(context)
|
||||
except Exception as e:
|
||||
tb = sys.exc_info()[2]
|
||||
|
||||
try:
|
||||
while tb.tb_next: # type: ignore[union-attr]
|
||||
tb = tb.tb_next # type: ignore[union-attr]
|
||||
|
||||
msg = {"lineno": tb.tb_lineno, "error": str(e)} # type: ignore[union-attr]
|
||||
return Response(json.dumps(msg), status=400, mimetype="application/json")
|
||||
finally:
|
||||
del tb
|
||||
93
src/flask_debugtoolbar/panels/timer.py
Normal file
93
src/flask_debugtoolbar/panels/timer.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from werkzeug import Request
|
||||
from werkzeug import Response
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
try:
|
||||
import resource
|
||||
|
||||
HAVE_RESOURCE = True
|
||||
except ImportError:
|
||||
HAVE_RESOURCE = False
|
||||
|
||||
|
||||
class TimerDebugPanel(DebugPanel):
|
||||
"""Panel that displays the time a response took in milliseconds."""
|
||||
|
||||
name = "Timer"
|
||||
has_content = HAVE_RESOURCE
|
||||
|
||||
def process_request(self, request: Request) -> None:
|
||||
self._start_time = time.time()
|
||||
|
||||
if HAVE_RESOURCE:
|
||||
self._start_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
|
||||
def process_response(self, request: Request, response: Response) -> None:
|
||||
self.total_time: float = (time.time() - self._start_time) * 1000
|
||||
|
||||
if HAVE_RESOURCE:
|
||||
self._end_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "Time"
|
||||
|
||||
def nav_subtitle(self) -> str:
|
||||
if not HAVE_RESOURCE:
|
||||
return f"TOTAL: {self.total_time:0.2f}ms"
|
||||
|
||||
utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime
|
||||
stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime
|
||||
return f"CPU: {(utime + stime) * 1000.0:0.2f}ms ({self.total_time:0.2f}ms)"
|
||||
|
||||
def title(self) -> str:
|
||||
return "Resource Usage"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def _elapsed_ru(self, name: str) -> float:
|
||||
return getattr(self._end_rusage, name) - getattr(self._start_rusage, name) # type: ignore[no-any-return]
|
||||
|
||||
def content(self) -> str:
|
||||
utime = 1000 * self._elapsed_ru("ru_utime")
|
||||
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")
|
||||
|
||||
# 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
|
||||
# I noticed that I was getting nothing but zeroes and that the docs agreed. :-(
|
||||
# blkin = self._elapsed_ru("ru_inblock")
|
||||
# blkout = self._elapsed_ru("ru_oublock")
|
||||
# swap = self._elapsed_ru("ru_nswap")
|
||||
# rss = self._end_rusage.ru_maxrss
|
||||
# srss = self._end_rusage.ru_ixrss
|
||||
# urss = self._end_rusage.ru_idrss
|
||||
# usrss = self._end_rusage.ru_isrss
|
||||
rows = (
|
||||
("User CPU time", f"{utime:0.3f} msec"),
|
||||
("System CPU time", f"{stime:0.3f} msec"),
|
||||
("Total CPU time", f"{(utime + stime):0.3f} msec"),
|
||||
("Elapsed time", f"{self.total_time:0.3f} msec"),
|
||||
("Context switches", f"{vcsw} voluntary, {ivcsw} involuntary"),
|
||||
# (
|
||||
# "Memory use",
|
||||
# f"{rss} max RSS, {srss} shared, {urss + usrss} unshared",
|
||||
# ),
|
||||
# ("Page faults", f"{minflt} no i/o, {majflt} requiring i/o"),
|
||||
# ("Disk operations", f"{blkin} in, {blkout} out, {swap} swapout"),
|
||||
)
|
||||
context = self.context.copy()
|
||||
context.update(
|
||||
{
|
||||
"rows": rows,
|
||||
}
|
||||
)
|
||||
return self.render("panels/timer.html", context)
|
||||
39
src/flask_debugtoolbar/panels/versions.py
Normal file
39
src/flask_debugtoolbar/panels/versions.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.metadata
|
||||
import os
|
||||
from sysconfig import get_path
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
flask_version: str = importlib.metadata.version("flask")
|
||||
|
||||
|
||||
class VersionDebugPanel(DebugPanel):
|
||||
"""Panel that displays the Flask version."""
|
||||
|
||||
name = "Version"
|
||||
has_content = True
|
||||
|
||||
def nav_title(self) -> str:
|
||||
return "Versions"
|
||||
|
||||
def nav_subtitle(self) -> str:
|
||||
return f"Flask {flask_version}"
|
||||
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def title(self) -> str:
|
||||
return "Versions"
|
||||
|
||||
def content(self) -> str:
|
||||
packages_metadata = [p.metadata for p in importlib.metadata.distributions()]
|
||||
packages = sorted(packages_metadata, key=lambda p: p["Name"].lower())
|
||||
return self.render(
|
||||
"panels/versions.html",
|
||||
{
|
||||
"packages": packages,
|
||||
"python_lib_dir": os.path.normpath(get_path("platlib")),
|
||||
},
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user