mirror of
https://github.com/pallets-eco/flask-debugtoolbar.git
synced 2026-01-05 21:20:12 -06:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e22ee143c | ||
|
|
38a7511f87 | ||
|
|
40f8645ec9 | ||
|
|
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 |
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
|
||||||
54
.github/workflows/publish.yaml
vendored
Normal file
54
.github/workflows/publish.yaml
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
name: Publish
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.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
|
||||||
|
- uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
|
||||||
|
with:
|
||||||
|
path: ./dist
|
||||||
|
create-release:
|
||||||
|
needs: [build]
|
||||||
|
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 }} artifact/*
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
publish-pypi:
|
||||||
|
needs: [build]
|
||||||
|
# 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@8a08d616893759ef8e1aa1f2785787c0b97e20d6 # v1.10.0
|
||||||
|
with:
|
||||||
|
repository-url: https://test.pypi.org/legacy/
|
||||||
|
packages-dir: artifact/
|
||||||
|
- uses: pypa/gh-action-pypi-publish@8a08d616893759ef8e1aa1f2785787c0b97e20d6 # v1.10.0
|
||||||
|
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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
|
||||||
|
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
cache: pip
|
||||||
|
cache-dependency-path: requirements*/*.txt
|
||||||
|
- name: cache mypy
|
||||||
|
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||||
|
with:
|
||||||
|
path: ./.mypy_cache
|
||||||
|
key: mypy|${{ hashFiles('pyproject.toml') }}
|
||||||
|
- run: pip install tox
|
||||||
|
- run: tox run -e typing
|
||||||
41
.github/workflows/tests.yml
vendored
41
.github/workflows/tests.yml
vendored
@@ -1,41 +0,0 @@
|
|||||||
name: Tests
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
paths-ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
- '*.rst'
|
|
||||||
pull_request:
|
|
||||||
paths-ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
- '*.rst'
|
|
||||||
jobs:
|
|
||||||
tests:
|
|
||||||
name: ${{ matrix.name }}
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- {name: Linux, python: '3.12', os: ubuntu-latest, tox: py312}
|
|
||||||
- {name: Windows, python: '3.12', os: windows-latest, tox: py312}
|
|
||||||
- {name: Mac, python: '3.12', os: macos-latest, tox: py312}
|
|
||||||
- {name: Minimal, python: '3.12', os: ubuntu-latest, tox: minimal}
|
|
||||||
- {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311}
|
|
||||||
- {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310}
|
|
||||||
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
|
|
||||||
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
|
|
||||||
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
|
|
||||||
- {name: Style, python: '3.10', os: ubuntu-latest, tox: stylecheck}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python }}
|
|
||||||
- name: update pip
|
|
||||||
run: |
|
|
||||||
pip install -U setuptools wheel
|
|
||||||
python -m pip install -U pip
|
|
||||||
- run: pip install tox
|
|
||||||
- run: tox -e ${{ matrix.tox }}
|
|
||||||
160
.gitignore
vendored
160
.gitignore
vendored
@@ -1,160 +1,10 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.venv*/
|
||||||
|
venv*/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
.coverage*
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
htmlcov/
|
||||||
.tox/
|
.tox/
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
|
||||||
db.sqlite3-journal
|
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
docs/_build/
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
# pyenv
|
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
|
||||||
# .python-version
|
|
||||||
|
|
||||||
# pipenv
|
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
||||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
||||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
||||||
# install all needed dependencies.
|
|
||||||
#Pipfile.lock
|
|
||||||
|
|
||||||
# poetry
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
||||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
||||||
# commonly ignored for libraries.
|
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
||||||
#poetry.lock
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
#pdm.lock
|
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|||||||
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)"
|
||||||
@@ -1,37 +1,13 @@
|
|||||||
# Read the Docs configuration file for Sphinx projects
|
|
||||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
||||||
|
|
||||||
# Required
|
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
# Set the OS, Python version and other tools you might need
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-lts-latest
|
os: ubuntu-22.04
|
||||||
tools:
|
tools:
|
||||||
python: "latest"
|
python: '3.12'
|
||||||
# You can also specify other tool versions:
|
|
||||||
# nodejs: "20"
|
|
||||||
# rust: "1.70"
|
|
||||||
# golang: "1.20"
|
|
||||||
|
|
||||||
# Build documentation in the "docs/" directory with Sphinx
|
|
||||||
sphinx:
|
|
||||||
configuration: docs/conf.py
|
|
||||||
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
|
|
||||||
# builder: "dirhtml"
|
|
||||||
# Fail on all warnings to avoid broken references
|
|
||||||
fail_on_warning: true
|
|
||||||
|
|
||||||
# Optionally build your docs in additional formats such as PDF and ePub
|
|
||||||
# formats:
|
|
||||||
# - pdf
|
|
||||||
# - epub
|
|
||||||
|
|
||||||
# Optional but recommended, declare the Python requirements required
|
|
||||||
# to build your documentation
|
|
||||||
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
|
||||||
python:
|
python:
|
||||||
install:
|
install:
|
||||||
- path: .
|
- requirements: requirements/docs.txt
|
||||||
extra_requirements:
|
- method: pip
|
||||||
- docs
|
path: .
|
||||||
|
sphinx:
|
||||||
|
builder: dirhtml
|
||||||
|
fail_on_warning: true
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
Copyright (c) Rob Hudson and individual contributors.
|
Copyright 2011 Pallets Community Ecosystem
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification,
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
are permitted provided that the following conditions are met:
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice,
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
this list of conditions and the following disclaimer.
|
list of conditions and the following disclaimer.
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
2. Redistributions in binary form must reproduce the above copyright
|
this list of conditions and the following disclaimer in the documentation
|
||||||
notice, this list of conditions and the following disclaimer in the
|
and/or other materials provided with the distribution.
|
||||||
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
|
||||||
3. Neither the name of Django nor the names of its contributors may be used
|
without specific prior written permission.
|
||||||
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
|
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
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
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
|
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
include LICENSE
|
|
||||||
recursive-include src/flask_debugtoolbar/templates *.html
|
|
||||||
recursive-include src/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/jazzband/django-debug-toolbar>`_
|
|
||||||
for Flask applications.
|
|
||||||
|
|
||||||
.. image:: https://github.com/pallets-eco/flask-debugtoolbar/actions/workflows/tests.yml/badge.svg
|
|
||||||
:target: https://github.com/pallets-eco/flask-debugtoolbar/actions
|
|
||||||
|
|
||||||
|
|
||||||
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
279
docs/conf.py
279
docs/conf.py
@@ -1,260 +1,39 @@
|
|||||||
# -*- coding: utf-8 -*-
|
import importlib.metadata
|
||||||
#
|
|
||||||
# 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 datetime
|
# Project --------------------------------------------------------------
|
||||||
import os
|
|
||||||
import pkg_resources
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
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())))
|
default_role = "code"
|
||||||
|
|
||||||
# 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.
|
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.viewcode',
|
"sphinx.ext.autodoc",
|
||||||
'sphinx.ext.intersphinx',
|
"sphinx.ext.extlinks",
|
||||||
'pallets_sphinx_themes',
|
"sphinx.ext.intersphinx",
|
||||||
|
"sphinxcontrib.log_cabinet",
|
||||||
|
"pallets_sphinx_themes",
|
||||||
]
|
]
|
||||||
|
autodoc_member_order = "bysource"
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
autodoc_typehints = "description"
|
||||||
templates_path = ['_templates']
|
autodoc_preserve_defaults = True
|
||||||
|
extlinks = {
|
||||||
# The suffix of source filenames.
|
"issue": ("https://github.com/pallets-eco/flask-debugtoolbar/issues/%s", "#%s"),
|
||||||
source_suffix = '.rst'
|
"pr": ("https://github.com/pallets-eco/flask-debugtoolbar/pull/%s", "#%s"),
|
||||||
|
}
|
||||||
# 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 = []
|
|
||||||
|
|
||||||
intersphinx_mapping = {
|
intersphinx_mapping = {
|
||||||
'flasksqlalchemy': ('https://flask-sqlalchemy.palletsprojects.com/', None)
|
"python": ("https://docs.python.org/3/", None),
|
||||||
|
"flasksqlalchemy": ("https://flask-sqlalchemy.palletsprojects.com", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# HTML -----------------------------------------------------------------
|
||||||
|
|
||||||
# -- Options for HTML output ---------------------------------------------------
|
html_theme = "flask"
|
||||||
|
html_static_path = ["_static"]
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
html_copy_source = False
|
||||||
# a list of builtin themes.
|
html_show_copyright = False
|
||||||
html_theme = 'flask'
|
html_use_index = False
|
||||||
|
html_domain_indices = False
|
||||||
# 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 = {}
|
|
||||||
|
|
||||||
# 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'
|
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ Name Description De
|
|||||||
==================================== ===================================== ==========================
|
==================================== ===================================== ==========================
|
||||||
``DEBUG_TB_ENABLED`` Enable the toolbar? ``app.debug``
|
``DEBUG_TB_ENABLED`` Enable the toolbar? ``app.debug``
|
||||||
``DEBUG_TB_HOSTS`` Whitelist of hosts to display toolbar any host
|
``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_INTERCEPT_REDIRECTS`` Should intercept redirects? ``True``
|
||||||
``DEBUG_TB_PANELS`` List of module/class names of panels enable all built-in panels
|
``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_PROFILER_ENABLED`` Enable the profiler on all requests ``False``, user-enabled
|
||||||
@@ -73,7 +80,8 @@ Panels
|
|||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
||||||
panels
|
panels
|
||||||
|
license
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
------------
|
------------
|
||||||
@@ -88,11 +96,3 @@ This was based on the original `django-debug-toolbar`_. Thanks to `Michael van T
|
|||||||
.. _django-debug-toolbar: https://github.com/jazzband/django-debug-toolbar
|
.. _django-debug-toolbar: https://github.com/jazzband/django-debug-toolbar
|
||||||
.. _Michael van Tellingen: https://github.com/mvantellingen
|
.. _Michael van Tellingen: https://github.com/mvantellingen
|
||||||
.. _individual contributors: https://github.com/pallets-eco/flask-debugtoolbar/graphs/contributors
|
.. _individual contributors: https://github.com/pallets-eco/flask-debugtoolbar/graphs/contributors
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
# Run using: `FLASK_DEBUG=True 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_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
from flask_debugtoolbar import DebugToolbarExtension
|
from flask_debugtoolbar import DebugToolbarExtension
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = True
|
app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = True
|
||||||
#app.config['DEBUG_TB_PANELS'] = (
|
# app.config['DEBUG_TB_PANELS'] = (
|
||||||
# 'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
|
# 'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
|
||||||
# 'flask_debugtoolbar.panels.logger.LoggingPanel',
|
# 'flask_debugtoolbar.panels.logger.LoggingPanel',
|
||||||
# 'flask_debugtoolbar.panels.timer.TimerDebugPanel',
|
# 'flask_debugtoolbar.panels.timer.TimerDebugPanel',
|
||||||
#)
|
# )
|
||||||
#app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' )
|
# app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' )
|
||||||
app.config['SECRET_KEY'] = 'asd'
|
app.config["SECRET_KEY"] = "asd"
|
||||||
app.config['SQLALCHEMY_RECORD_QUERIES'] = True
|
app.config["SQLALCHEMY_RECORD_QUERIES"] = True
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
|
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:
|
# 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
|
# app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
@@ -24,21 +28,21 @@ toolbar = DebugToolbarExtension(app)
|
|||||||
|
|
||||||
|
|
||||||
class ExampleModel(db.Model):
|
class ExampleModel(db.Model):
|
||||||
__tablename__ = 'examples'
|
__tablename__ = "examples"
|
||||||
value = db.Column(db.String(100), primary_key=True)
|
value = db.Column(db.String(100), primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
app.logger.info("Hello there")
|
app.logger.info("Hello there")
|
||||||
ExampleModel.query.get(1)
|
ExampleModel.query.get(1)
|
||||||
return render_template('index.html')
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/redirect')
|
@app.route("/redirect")
|
||||||
def redirect_example():
|
def redirect_example():
|
||||||
response = redirect(url_for('index'))
|
response = redirect(url_for("index"))
|
||||||
response.set_cookie('test_cookie', '1')
|
response.set_cookie("test_cookie", "1")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,84 @@
|
|||||||
[build-system]
|
[project]
|
||||||
requires = [
|
name = "Flask-DebugToolbar"
|
||||||
"setuptools>=42",
|
version = "0.16.0"
|
||||||
"wheel"
|
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
|
||||||
36
setup.cfg
36
setup.cfg
@@ -1,36 +0,0 @@
|
|||||||
[metadata]
|
|
||||||
name = Flask-DebugToolbar
|
|
||||||
version = 0.15.1
|
|
||||||
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/pallets-eco/flask-debugtoolbar
|
|
||||||
project_urls =
|
|
||||||
Changelog = https://github.com/pallets-eco/flask-debugtoolbar/releases
|
|
||||||
Documentation = https://flask-debugtoolbar.readthedocs.io/
|
|
||||||
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]
|
|
||||||
package_dir =
|
|
||||||
= src
|
|
||||||
|
|
||||||
packages = find:
|
|
||||||
include_package_data = True
|
|
||||||
python_requires = >=3.7
|
|
||||||
|
|
||||||
[options.packages.find]
|
|
||||||
where = src
|
|
||||||
20
setup.py
20
setup.py
@@ -1,20 +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>=2.2.0',
|
|
||||||
'Blinker',
|
|
||||||
'itsdangerous',
|
|
||||||
'werkzeug',
|
|
||||||
'MarkupSafe',
|
|
||||||
'packaging',
|
|
||||||
],
|
|
||||||
extras_require={
|
|
||||||
"docs": [
|
|
||||||
'Sphinx>=1.2.2',
|
|
||||||
'Pallets-Sphinx-Themes',
|
|
||||||
],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
@@ -1,95 +1,105 @@
|
|||||||
import contextvars
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections.abc as c
|
||||||
import os
|
import os
|
||||||
|
import typing as t
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import warnings
|
import warnings
|
||||||
|
from contextvars import ContextVar
|
||||||
|
|
||||||
import flask
|
import jinja2.ext
|
||||||
from flask import Blueprint, current_app, request, g, send_from_directory, url_for
|
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 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 jinja2 import __version__ as __jinja_version__
|
from .toolbar import DebugToolbar
|
||||||
from jinja2 import Environment, PackageLoader
|
from .utils import decode_text
|
||||||
|
from .utils import gzip_compress
|
||||||
|
from .utils import gzip_decompress
|
||||||
|
|
||||||
from flask_debugtoolbar.compat import iteritems
|
module: Blueprint = Blueprint("debugtoolbar", __name__)
|
||||||
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: str, target: str, replacement: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def replace_insensitive(string, target, replacement):
|
|
||||||
"""Similar to string.replace() but is case insensitive
|
"""Similar to string.replace() but is case insensitive
|
||||||
Code borrowed from:
|
Code borrowed from:
|
||||||
http://forums.devshed.com/python-programming-11/case-insensitive-string-replace-490921.html
|
http://forums.devshed.com/python-programming-11/case-insensitive-string-replace-490921.html
|
||||||
"""
|
"""
|
||||||
no_case = string.lower()
|
no_case = string.lower()
|
||||||
index = no_case.rfind(target.lower())
|
index = no_case.rfind(target.lower())
|
||||||
|
|
||||||
if index >= 0:
|
if index >= 0:
|
||||||
return string[:index] + replacement + string[index + len(target):]
|
return string[:index] + replacement + string[index + len(target) :]
|
||||||
else: # no results so return the original string
|
else: # no results so return the original string
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
def _printable(value):
|
def _printable(value: object) -> str:
|
||||||
try:
|
try:
|
||||||
return decode_text(repr(value))
|
return decode_text(repr(value))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return '<repr(%s) raised %s: %s>' % (
|
return f"<repr({object.__repr__(value)}) raised {type(e).__name__}: {e}>"
|
||||||
object.__repr__(value), type(e).__name__, e)
|
|
||||||
|
|
||||||
|
|
||||||
class DebugToolbarExtension(object):
|
class DebugToolbarExtension:
|
||||||
_static_dir = os.path.realpath(
|
_static_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "static"))
|
||||||
os.path.join(os.path.dirname(__file__), 'static'))
|
|
||||||
|
|
||||||
_toolbar_codes = [200, 201, 400, 401, 403, 404, 405, 500, 501, 502, 503, 504]
|
_toolbar_codes = [200, 201, 400, 401, 403, 404, 405, 500, 501, 502, 503, 504]
|
||||||
_redirect_codes = [301, 302, 303, 304]
|
_redirect_codes = [301, 302, 303, 304]
|
||||||
|
|
||||||
def __init__(self, app=None):
|
def __init__(self, app: Flask | None = None) -> None:
|
||||||
self.app = app
|
self.app = app
|
||||||
|
self.toolbar_routes_host: str | None = None
|
||||||
|
|
||||||
# Support threads running `flask.copy_current_request_context` without
|
# Support threads running `flask.copy_current_request_context` without
|
||||||
# poping toolbar during `teardown_request`
|
# poping toolbar during `teardown_request`
|
||||||
self.debug_toolbars_var = contextvars.ContextVar('debug_toolbars')
|
self.debug_toolbars_var: ContextVar[dict[Request, DebugToolbar]] = ContextVar(
|
||||||
jinja_extensions = ['jinja2.ext.i18n']
|
"debug_toolbars"
|
||||||
|
)
|
||||||
|
jinja_extensions = [jinja2.ext.i18n]
|
||||||
|
|
||||||
if __jinja_version__[0] == '2':
|
# Jinja2<3
|
||||||
jinja_extensions.append('jinja2.ext.with_')
|
if hasattr(jinja2.ext, "with_"):
|
||||||
|
jinja_extensions.append(jinja2.ext.with_) # pyright: ignore
|
||||||
|
|
||||||
# Configure jinja for the internal templates and add url rules
|
# Configure jinja for the internal templates and add url rules
|
||||||
# for static data
|
# for static data
|
||||||
self.jinja_env = Environment(
|
self.jinja_env: Environment = Environment(
|
||||||
autoescape=True,
|
autoescape=True,
|
||||||
extensions=jinja_extensions,
|
extensions=jinja_extensions,
|
||||||
loader=PackageLoader(__name__, 'templates'))
|
loader=PackageLoader(__name__, "templates"),
|
||||||
self.jinja_env.filters['urlencode'] = urllib.parse.quote_plus
|
)
|
||||||
self.jinja_env.filters['printable'] = _printable
|
self.jinja_env.filters["urlencode"] = urllib.parse.quote_plus
|
||||||
self.jinja_env.globals['url_for'] = url_for
|
self.jinja_env.filters["printable"] = _printable
|
||||||
|
self.jinja_env.globals["url_for"] = url_for
|
||||||
|
|
||||||
if app is not None:
|
if app is not None:
|
||||||
self.init_app(app)
|
self.init_app(app)
|
||||||
|
|
||||||
def init_app(self, app):
|
def init_app(self, app: Flask) -> None:
|
||||||
for k, v in iteritems(self._default_config(app)):
|
for k, v in self._default_config(app).items():
|
||||||
app.config.setdefault(k, v)
|
app.config.setdefault(k, v)
|
||||||
|
|
||||||
if not app.config['DEBUG_TB_ENABLED']:
|
if not app.config["DEBUG_TB_ENABLED"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not app.config.get('SECRET_KEY'):
|
if not app.config.get("SECRET_KEY"):
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"The Flask-DebugToolbar requires the 'SECRET_KEY' config "
|
"The Flask-DebugToolbar requires the 'SECRET_KEY' config "
|
||||||
"var to be set")
|
"var to be set"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._validate_and_configure_toolbar_routes_host(app)
|
||||||
|
|
||||||
DebugToolbar.load_panels(app)
|
DebugToolbar.load_panels(app)
|
||||||
|
|
||||||
@@ -98,90 +108,161 @@ class DebugToolbarExtension(object):
|
|||||||
app.teardown_request(self.teardown_request)
|
app.teardown_request(self.teardown_request)
|
||||||
|
|
||||||
# Monkey-patch the Flask.dispatch_request method
|
# Monkey-patch the Flask.dispatch_request method
|
||||||
app.dispatch_request = self.dispatch_request
|
app.dispatch_request = self.dispatch_request # type: ignore[method-assign]
|
||||||
|
|
||||||
app.add_url_rule('/_debug_toolbar/static/<path:filename>',
|
app.add_url_rule(
|
||||||
'_debug_toolbar.static', self.send_static_file)
|
"/_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')
|
app.register_blueprint(module, url_prefix="/_debug_toolbar/views")
|
||||||
|
|
||||||
def _default_config(self, app):
|
def _default_config(self, app: Flask) -> dict[str, t.Any]:
|
||||||
return {
|
return {
|
||||||
'DEBUG_TB_ENABLED': app.debug,
|
"DEBUG_TB_ENABLED": app.debug,
|
||||||
'DEBUG_TB_HOSTS': (),
|
"DEBUG_TB_HOSTS": (),
|
||||||
'DEBUG_TB_INTERCEPT_REDIRECTS': True,
|
"DEBUG_TB_ROUTES_HOST": None,
|
||||||
'DEBUG_TB_PANELS': (
|
"DEBUG_TB_INTERCEPT_REDIRECTS": True,
|
||||||
'flask_debugtoolbar.panels.versions.VersionDebugPanel',
|
"DEBUG_TB_PANELS": (
|
||||||
'flask_debugtoolbar.panels.timer.TimerDebugPanel',
|
"flask_debugtoolbar.panels.versions.VersionDebugPanel",
|
||||||
'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
|
"flask_debugtoolbar.panels.timer.TimerDebugPanel",
|
||||||
'flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel',
|
"flask_debugtoolbar.panels.headers.HeaderDebugPanel",
|
||||||
'flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel',
|
"flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel",
|
||||||
'flask_debugtoolbar.panels.template.TemplateDebugPanel',
|
"flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel",
|
||||||
'flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel',
|
"flask_debugtoolbar.panels.template.TemplateDebugPanel",
|
||||||
'flask_debugtoolbar.panels.logger.LoggingPanel',
|
"flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel",
|
||||||
'flask_debugtoolbar.panels.route_list.RouteListDebugPanel',
|
"flask_debugtoolbar.panels.logger.LoggingPanel",
|
||||||
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel',
|
"flask_debugtoolbar.panels.route_list.RouteListDebugPanel",
|
||||||
'flask_debugtoolbar.panels.g.GDebugPanel',
|
"flask_debugtoolbar.panels.profiler.ProfilerDebugPanel",
|
||||||
|
"flask_debugtoolbar.panels.g.GDebugPanel",
|
||||||
),
|
),
|
||||||
'SQLALCHEMY_RECORD_QUERIES': app.debug,
|
"SQLALCHEMY_RECORD_QUERIES": app.debug,
|
||||||
}
|
}
|
||||||
|
|
||||||
def dispatch_request(self):
|
def _validate_and_configure_toolbar_routes_host(self, app: Flask) -> None:
|
||||||
"""Modified version of Flask.dispatch_request to call process_view."""
|
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
|
req = request_ctx.request
|
||||||
app = current_app
|
|
||||||
|
|
||||||
if req.routing_exception is not None:
|
if req.routing_exception is not None:
|
||||||
app.raise_routing_exception(req)
|
app.raise_routing_exception(req)
|
||||||
|
|
||||||
rule = req.url_rule
|
rule: Rule = req.url_rule # type: ignore[assignment]
|
||||||
|
|
||||||
# if we provide automatic options for this URL and the
|
if (
|
||||||
# request came with the OPTIONS method, reply automatically
|
getattr(rule, "provide_automatic_options", False)
|
||||||
if getattr(rule, 'provide_automatic_options', False) \
|
and req.method == "OPTIONS"
|
||||||
and req.method == 'OPTIONS':
|
):
|
||||||
return app.make_default_options_response()
|
return app.make_default_options_response()
|
||||||
|
|
||||||
# otherwise dispatch to the handler for that endpoint
|
|
||||||
view_func = app.view_functions[rule.endpoint]
|
view_func = app.view_functions[rule.endpoint]
|
||||||
view_func = self.process_view(app, view_func, req.view_args)
|
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)
|
||||||
|
|
||||||
return view_func(**req.view_args)
|
def _show_toolbar(self) -> bool:
|
||||||
|
|
||||||
def _show_toolbar(self):
|
|
||||||
"""Return a boolean to indicate if we need to show the toolbar."""
|
"""Return a boolean to indicate if we need to show the toolbar."""
|
||||||
if request.blueprint == 'debugtoolbar':
|
if request.blueprint == "debugtoolbar":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
hosts = current_app.config['DEBUG_TB_HOSTS']
|
hosts = current_app.config["DEBUG_TB_HOSTS"]
|
||||||
|
|
||||||
if hosts and request.remote_addr not in hosts:
|
if hosts and request.remote_addr not in hosts:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def send_static_file(self, filename):
|
def send_static_file(self, filename: str) -> Response:
|
||||||
"""Send a static file from the flask-debugtoolbar static directory."""
|
"""Send a static file from the flask-debugtoolbar static directory."""
|
||||||
return send_from_directory(self._static_dir, filename)
|
return send_from_directory(self._static_dir, filename)
|
||||||
|
|
||||||
def process_request(self):
|
def process_request(self) -> None:
|
||||||
g.debug_toolbar = self
|
g.debug_toolbar = self
|
||||||
|
|
||||||
if not self._show_toolbar():
|
if not self._show_toolbar():
|
||||||
return
|
return
|
||||||
|
|
||||||
real_request = request._get_current_object()
|
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||||
self.debug_toolbars_var.set({})
|
self.debug_toolbars_var.set({})
|
||||||
self.debug_toolbars_var.get()[real_request] = (
|
self.debug_toolbars_var.get()[real_request] = DebugToolbar(
|
||||||
DebugToolbar(real_request, self.jinja_env))
|
real_request, self.jinja_env
|
||||||
|
)
|
||||||
|
|
||||||
for panel in self.debug_toolbars_var.get()[real_request].panels:
|
for panel in self.debug_toolbars_var.get()[real_request].panels:
|
||||||
panel.process_request(real_request)
|
panel.process_request(real_request)
|
||||||
|
|
||||||
def process_view(self, app, view_func, view_kwargs):
|
def process_view(
|
||||||
""" This method is called just before the flask view is called.
|
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.
|
This is done by the dispatch_request method.
|
||||||
"""
|
"""
|
||||||
real_request = request._get_current_object()
|
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
toolbar = self.debug_toolbars_var.get({})[real_request]
|
toolbar = self.debug_toolbars_var.get({})[real_request]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -189,57 +270,65 @@ class DebugToolbarExtension(object):
|
|||||||
|
|
||||||
for panel in toolbar.panels:
|
for panel in toolbar.panels:
|
||||||
new_view = panel.process_view(real_request, view_func, view_kwargs)
|
new_view = panel.process_view(real_request, view_func, view_kwargs)
|
||||||
|
|
||||||
if new_view:
|
if new_view:
|
||||||
view_func = new_view
|
view_func = new_view
|
||||||
|
|
||||||
return view_func
|
return view_func
|
||||||
|
|
||||||
def process_response(self, response):
|
def process_response(self, response: Response) -> Response:
|
||||||
real_request = request._get_current_object()
|
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||||
|
|
||||||
if real_request not in self.debug_toolbars_var.get({}):
|
if real_request not in self.debug_toolbars_var.get({}):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
# Intercept http redirect codes and display an html page with a
|
# Intercept http redirect codes and display an html page with a
|
||||||
# link to the target.
|
# link to the target.
|
||||||
if current_app.config['DEBUG_TB_INTERCEPT_REDIRECTS']:
|
if current_app.config["DEBUG_TB_INTERCEPT_REDIRECTS"]:
|
||||||
if response.status_code in self._redirect_codes:
|
if response.status_code in self._redirect_codes:
|
||||||
redirect_to = response.location
|
redirect_to = response.location
|
||||||
redirect_code = response.status_code
|
redirect_code = response.status_code
|
||||||
|
|
||||||
if redirect_to:
|
if redirect_to:
|
||||||
content = self.render('redirect.html', {
|
content = self.render(
|
||||||
'redirect_to': redirect_to,
|
"redirect.html",
|
||||||
'redirect_code': redirect_code
|
{"redirect_to": redirect_to, "redirect_code": redirect_code},
|
||||||
})
|
)
|
||||||
response.content_length = len(content)
|
response.content_length = len(content)
|
||||||
response.location = None
|
del response.location
|
||||||
response.response = [content]
|
response.response = [content]
|
||||||
response.status_code = 200
|
response.status_code = 200
|
||||||
|
|
||||||
# If the http response code is an allowed code then we process to add the
|
# If the http response code is an allowed code then we process to add the
|
||||||
# toolbar to the returned html response.
|
# toolbar to the returned html response.
|
||||||
if not (response.status_code in self._toolbar_codes and
|
if not (
|
||||||
response.is_sequence and
|
response.status_code in self._toolbar_codes
|
||||||
response.headers['content-type'].startswith('text/html')):
|
and response.is_sequence
|
||||||
|
and response.headers["content-type"].startswith("text/html")
|
||||||
|
):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
content_encoding = response.headers.get('Content-Encoding')
|
content_encoding = response.headers.get("Content-Encoding")
|
||||||
if content_encoding and 'gzip' in content_encoding:
|
|
||||||
|
if content_encoding and "gzip" in content_encoding:
|
||||||
response_html = gzip_decompress(response.data).decode()
|
response_html = gzip_decompress(response.data).decode()
|
||||||
else:
|
else:
|
||||||
response_html = response.get_data(as_text=True)
|
response_html = response.get_data(as_text=True)
|
||||||
|
|
||||||
no_case = response_html.lower()
|
no_case = response_html.lower()
|
||||||
body_end = no_case.rfind('</body>')
|
body_end = no_case.rfind("</body>")
|
||||||
|
|
||||||
if body_end >= 0:
|
if body_end >= 0:
|
||||||
before = response_html[:body_end]
|
before = response_html[:body_end]
|
||||||
after = response_html[body_end:]
|
after = response_html[body_end:]
|
||||||
elif no_case.startswith('<!doctype html>'):
|
elif no_case.startswith("<!doctype html>"):
|
||||||
before = response_html
|
before = response_html
|
||||||
after = ''
|
after = ""
|
||||||
else:
|
else:
|
||||||
warnings.warn('Could not insert debug toolbar.'
|
warnings.warn(
|
||||||
' </body> tag not found in response.')
|
"Could not insert debug toolbar." " </body> tag not found in response.",
|
||||||
|
stacklevel=1,
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
toolbar = self.debug_toolbars_var.get()[real_request]
|
toolbar = self.debug_toolbars_var.get()[real_request]
|
||||||
@@ -249,19 +338,22 @@ class DebugToolbarExtension(object):
|
|||||||
|
|
||||||
toolbar_html = toolbar.render_toolbar()
|
toolbar_html = toolbar.render_toolbar()
|
||||||
|
|
||||||
content = ''.join((before, toolbar_html, after))
|
content = "".join((before, toolbar_html, after))
|
||||||
content = content.encode('utf-8')
|
content_bytes = content.encode("utf-8")
|
||||||
if content_encoding and 'gzip' in content_encoding:
|
|
||||||
content = gzip_compress(content)
|
if content_encoding and "gzip" in content_encoding:
|
||||||
response.response = [content]
|
content_bytes = gzip_compress(content_bytes)
|
||||||
response.content_length = len(content)
|
|
||||||
|
response.response = [content_bytes]
|
||||||
|
response.content_length = len(content_bytes)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def teardown_request(self, exc):
|
def teardown_request(self, exc: BaseException | None) -> None:
|
||||||
# debug_toolbars_var won't be set under `flask.copy_current_request_context`
|
# debug_toolbars_var won't be set under `flask.copy_current_request_context`
|
||||||
self.debug_toolbars_var.get({}).pop(request._get_current_object(), None)
|
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||||
|
self.debug_toolbars_var.get({}).pop(real_request, None)
|
||||||
|
|
||||||
def render(self, template_name, context):
|
def render(self, template_name: str, context: dict[str, t.Any]) -> str:
|
||||||
template = self.jinja_env.get_template(template_name)
|
template = self.jinja_env.get_template(template_name)
|
||||||
return template.render(**context)
|
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,11 +1,18 @@
|
|||||||
"""Base DebugPanel class"""
|
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(object):
|
class DebugPanel:
|
||||||
"""
|
"""Base class for debug panels."""
|
||||||
Base class for debug panels.
|
|
||||||
"""
|
name: str
|
||||||
# name = Base
|
|
||||||
|
|
||||||
# If content returns something, set to true in subclass
|
# If content returns something, set to true in subclass
|
||||||
has_content = False
|
has_content = False
|
||||||
@@ -15,18 +22,21 @@ class DebugPanel(object):
|
|||||||
|
|
||||||
# We'll maintain a local context instance so we can expose our template
|
# We'll maintain a local context instance so we can expose our template
|
||||||
# context variables to panels which need them:
|
# context variables to panels which need them:
|
||||||
context = {}
|
context: dict[str, t.Any] = {}
|
||||||
|
|
||||||
# Panel methods
|
# Panel methods
|
||||||
def __init__(self, jinja_env, context={}):
|
def __init__(
|
||||||
self.context.update(context)
|
self, jinja_env: Environment, context: dict[str, t.Any] | None = None
|
||||||
self.jinja_env = jinja_env
|
) -> None:
|
||||||
|
if context is not None:
|
||||||
|
self.context.update(context)
|
||||||
|
|
||||||
|
self.jinja_env = jinja_env
|
||||||
# If the client enabled the panel
|
# If the client enabled the panel
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def init_app(cls, app):
|
def init_app(cls, app: Flask) -> None:
|
||||||
"""Method that can be overridden by child classes.
|
"""Method that can be overridden by child classes.
|
||||||
Can be used for setting up additional URL-rules/routes.
|
Can be used for setting up additional URL-rules/routes.
|
||||||
|
|
||||||
@@ -48,37 +58,42 @@ class DebugPanel(object):
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def render(self, template_name, context):
|
def render(self, template_name: str, context: dict[str, t.Any]) -> str:
|
||||||
template = self.jinja_env.get_template(template_name)
|
template = self.jinja_env.get_template(template_name)
|
||||||
return template.render(**context)
|
return template.render(**context)
|
||||||
|
|
||||||
def dom_id(self):
|
def dom_id(self) -> str:
|
||||||
return 'flDebug%sPanel' % (self.name.replace(' ', ''))
|
return f"flDebug{self.name.replace(' ', '')}Panel"
|
||||||
|
|
||||||
def nav_title(self):
|
def nav_title(self) -> str:
|
||||||
"""Title showing in toolbar"""
|
"""Title showing in toolbar"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def nav_subtitle(self):
|
def nav_subtitle(self) -> str:
|
||||||
"""Subtitle showing until title in toolbar"""
|
"""Subtitle showing until title in toolbar"""
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
"""Title showing in panel"""
|
"""Title showing in panel"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def content(self):
|
def content(self) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
# Standard middleware methods
|
# Standard middleware methods
|
||||||
def process_request(self, request):
|
def process_request(self, request: Request) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def process_view(self, request, view_func, view_kwargs):
|
def process_view(
|
||||||
|
self,
|
||||||
|
request: Request,
|
||||||
|
view_func: c.Callable[..., t.Any],
|
||||||
|
view_kwargs: dict[str, t.Any],
|
||||||
|
) -> c.Callable[..., t.Any] | None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def process_response(self, request, response):
|
def process_response(self, request: Request, response: Response) -> None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
from flask import current_app
|
from __future__ import annotations
|
||||||
from flask_debugtoolbar.panels import DebugPanel
|
|
||||||
|
|
||||||
_ = lambda x: x
|
from flask import current_app
|
||||||
|
|
||||||
|
from . import DebugPanel
|
||||||
|
|
||||||
|
|
||||||
class ConfigVarsDebugPanel(DebugPanel):
|
class ConfigVarsDebugPanel(DebugPanel):
|
||||||
"""
|
"""A panel to display all variables from Flask configuration."""
|
||||||
A panel to display all variables from Flask configuration
|
|
||||||
"""
|
name = "ConfigVars"
|
||||||
name = 'ConfigVars'
|
|
||||||
has_content = True
|
has_content = True
|
||||||
|
|
||||||
def nav_title(self):
|
def nav_title(self) -> str:
|
||||||
return _('Config')
|
return "Config"
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
return _('Config')
|
return "Config"
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def content(self):
|
def content(self) -> str:
|
||||||
context = self.context.copy()
|
context = self.context.copy()
|
||||||
context.update({
|
context.update(
|
||||||
'config': current_app.config,
|
{
|
||||||
})
|
"config": current_app.config,
|
||||||
|
}
|
||||||
return self.render('panels/config_vars.html', context)
|
)
|
||||||
|
return self.render("panels/config_vars.html", context)
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
from flask import g
|
from __future__ import annotations
|
||||||
from flask_debugtoolbar.panels import DebugPanel
|
|
||||||
|
|
||||||
_ = lambda x: x
|
from flask import g
|
||||||
|
|
||||||
|
from . import DebugPanel
|
||||||
|
|
||||||
|
|
||||||
class GDebugPanel(DebugPanel):
|
class GDebugPanel(DebugPanel):
|
||||||
"""
|
"""A panel to display ``flask.g`` content."""
|
||||||
A panel to display flask.g content.
|
|
||||||
"""
|
name = "g"
|
||||||
name = 'g'
|
|
||||||
has_content = True
|
has_content = True
|
||||||
|
|
||||||
def nav_title(self):
|
def nav_title(self) -> str:
|
||||||
return _('flask.g')
|
return "flask.g"
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
return _('flask.g content')
|
return "flask.g content"
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def content(self):
|
def content(self) -> str:
|
||||||
context = self.context.copy()
|
context = self.context.copy()
|
||||||
context.update({
|
context.update({"g_content": g.__dict__})
|
||||||
'g_content': g.__dict__
|
return self.render("panels/g.html", context)
|
||||||
})
|
|
||||||
return self.render('panels/g.html', context)
|
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
from flask_debugtoolbar.panels import DebugPanel
|
from __future__ import annotations
|
||||||
|
|
||||||
_ = lambda x: x
|
import typing as t
|
||||||
|
|
||||||
|
from werkzeug import Request
|
||||||
|
|
||||||
|
from . import DebugPanel
|
||||||
|
|
||||||
|
|
||||||
class HeaderDebugPanel(DebugPanel):
|
class HeaderDebugPanel(DebugPanel):
|
||||||
"""
|
"""A panel to display HTTP headers."""
|
||||||
A panel to display HTTP headers.
|
|
||||||
"""
|
name = "Header"
|
||||||
name = 'Header'
|
|
||||||
has_content = True
|
has_content = True
|
||||||
# List of headers we want to display
|
# List of headers we want to display
|
||||||
header_filter = (
|
header_filter: tuple[str, ...] = (
|
||||||
'CONTENT_TYPE',
|
"CONTENT_TYPE",
|
||||||
'HTTP_ACCEPT',
|
"HTTP_ACCEPT",
|
||||||
'HTTP_ACCEPT_CHARSET',
|
"HTTP_ACCEPT_CHARSET",
|
||||||
'HTTP_ACCEPT_ENCODING',
|
"HTTP_ACCEPT_ENCODING",
|
||||||
'HTTP_ACCEPT_LANGUAGE',
|
"HTTP_ACCEPT_LANGUAGE",
|
||||||
'HTTP_CACHE_CONTROL',
|
"HTTP_CACHE_CONTROL",
|
||||||
'HTTP_CONNECTION',
|
"HTTP_CONNECTION",
|
||||||
'HTTP_HOST',
|
"HTTP_HOST",
|
||||||
'HTTP_KEEP_ALIVE',
|
"HTTP_KEEP_ALIVE",
|
||||||
'HTTP_REFERER',
|
"HTTP_REFERER",
|
||||||
'HTTP_USER_AGENT',
|
"HTTP_USER_AGENT",
|
||||||
'QUERY_STRING',
|
"QUERY_STRING",
|
||||||
'REMOTE_ADDR',
|
"REMOTE_ADDR",
|
||||||
'REMOTE_HOST',
|
"REMOTE_HOST",
|
||||||
'REQUEST_METHOD',
|
"REQUEST_METHOD",
|
||||||
'SCRIPT_NAME',
|
"SCRIPT_NAME",
|
||||||
'SERVER_NAME',
|
"SERVER_NAME",
|
||||||
'SERVER_PORT',
|
"SERVER_PORT",
|
||||||
'SERVER_PROTOCOL',
|
"SERVER_PROTOCOL",
|
||||||
'SERVER_SOFTWARE',
|
"SERVER_SOFTWARE",
|
||||||
)
|
)
|
||||||
|
|
||||||
def nav_title(self):
|
def nav_title(self) -> str:
|
||||||
return _('HTTP Headers')
|
return "HTTP Headers"
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
return _('HTTP Headers')
|
return "HTTP Headers"
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request: Request) -> None:
|
||||||
self.headers = dict(
|
self.headers: dict[str, t.Any] = {
|
||||||
[(k, request.environ[k])
|
k: request.environ[k] for k in self.header_filter if k in request.environ
|
||||||
for k in self.header_filter if k in request.environ]
|
}
|
||||||
)
|
|
||||||
|
|
||||||
def content(self):
|
def content(self) -> str:
|
||||||
context = self.context.copy()
|
context = self.context.copy()
|
||||||
context.update({
|
context.update({"headers": self.headers})
|
||||||
'headers': self.headers
|
return self.render("panels/headers.html", context)
|
||||||
})
|
|
||||||
return self.render('panels/headers.html', context)
|
|
||||||
|
|||||||
@@ -1,55 +1,57 @@
|
|||||||
from __future__ import with_statement
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
try:
|
import threading
|
||||||
import threading
|
|
||||||
except ImportError:
|
|
||||||
threading = None
|
|
||||||
|
|
||||||
from flask_debugtoolbar.panels import DebugPanel
|
from werkzeug import Request
|
||||||
from flask_debugtoolbar.utils import format_fname
|
|
||||||
|
|
||||||
_ = lambda x: x
|
from ..utils import format_fname
|
||||||
|
from . import DebugPanel
|
||||||
|
|
||||||
|
|
||||||
class ThreadTrackingHandler(logging.Handler):
|
class ThreadTrackingHandler(logging.Handler):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
if threading is None:
|
super().__init__()
|
||||||
raise NotImplementedError("threading module is not available, \
|
# a dictionary that maps threads to log records
|
||||||
the logging panel cannot be used without it")
|
self.records: dict[threading.Thread, list[logging.LogRecord]] = {}
|
||||||
logging.Handler.__init__(self)
|
|
||||||
self.records = {} # a dictionary that maps threads to log records
|
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
self.get_records().append(record)
|
self.get_records().append(record)
|
||||||
|
|
||||||
def get_records(self, thread=None):
|
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
|
Returns a list of records for the provided thread, of if none is
|
||||||
provided, returns a list for the current thread.
|
provided, returns a list for the current thread.
|
||||||
"""
|
"""
|
||||||
if thread is None:
|
if thread is None:
|
||||||
thread = threading.current_thread()
|
thread = threading.current_thread()
|
||||||
|
|
||||||
if thread not in self.records:
|
if thread not in self.records:
|
||||||
self.records[thread] = []
|
self.records[thread] = []
|
||||||
|
|
||||||
return self.records[thread]
|
return self.records[thread]
|
||||||
|
|
||||||
def clear_records(self, thread=None):
|
def clear_records(self, thread: threading.Thread | None = None) -> None:
|
||||||
if thread is None:
|
if thread is None:
|
||||||
thread = threading.current_thread()
|
thread = threading.current_thread()
|
||||||
|
|
||||||
if thread in self.records:
|
if thread in self.records:
|
||||||
del self.records[thread]
|
del self.records[thread]
|
||||||
|
|
||||||
|
|
||||||
handler = None
|
handler: ThreadTrackingHandler = None # type: ignore[assignment]
|
||||||
_init_lock = threading.Lock()
|
_init_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
def _init_once():
|
def _init_once() -> None:
|
||||||
global handler
|
global handler
|
||||||
|
|
||||||
if handler is not None:
|
if handler is not None:
|
||||||
return
|
return
|
||||||
|
|
||||||
with _init_lock:
|
with _init_lock:
|
||||||
if handler is not None:
|
if handler is not None:
|
||||||
return
|
return
|
||||||
@@ -59,57 +61,55 @@ def _init_once():
|
|||||||
# and not configure console logging for the request log.
|
# and not configure console logging for the request log.
|
||||||
# Werkzeug's default log level is INFO so this message probably won't
|
# Werkzeug's default log level is INFO so this message probably won't
|
||||||
# be seen.
|
# be seen.
|
||||||
try:
|
from werkzeug._internal import _log
|
||||||
from werkzeug._internal import _log
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
_log('debug', 'Initializing Flask-DebugToolbar log handler')
|
|
||||||
|
|
||||||
|
_log("debug", "Initializing Flask-DebugToolbar log handler")
|
||||||
handler = ThreadTrackingHandler()
|
handler = ThreadTrackingHandler()
|
||||||
logging.root.addHandler(handler)
|
logging.root.addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
class LoggingPanel(DebugPanel):
|
class LoggingPanel(DebugPanel):
|
||||||
name = 'Logging'
|
name = "Logging"
|
||||||
has_content = True
|
has_content = True
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request: Request) -> None:
|
||||||
_init_once()
|
_init_once()
|
||||||
handler.clear_records()
|
handler.clear_records()
|
||||||
|
|
||||||
def get_and_delete(self):
|
def get_and_delete(self) -> list[logging.LogRecord]:
|
||||||
records = handler.get_records()
|
records = handler.get_records()
|
||||||
handler.clear_records()
|
handler.clear_records()
|
||||||
return records
|
return records
|
||||||
|
|
||||||
def nav_title(self):
|
def nav_title(self) -> str:
|
||||||
return _("Logging")
|
return "Logging"
|
||||||
|
|
||||||
def nav_subtitle(self):
|
def nav_subtitle(self) -> str:
|
||||||
# FIXME l10n: use ngettext
|
|
||||||
num_records = len(handler.get_records())
|
num_records = len(handler.get_records())
|
||||||
return '%s message%s' % (num_records, '' if num_records == 1 else 's')
|
plural = "message" if num_records == 1 else "messages"
|
||||||
|
return f"{num_records} {plural}"
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
return _('Log Messages')
|
return "Log Messages"
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def content(self):
|
def content(self) -> str:
|
||||||
records = []
|
records = []
|
||||||
|
|
||||||
for record in self.get_and_delete():
|
for record in self.get_and_delete():
|
||||||
records.append({
|
records.append(
|
||||||
'message': record.getMessage(),
|
{
|
||||||
'time': datetime.datetime.fromtimestamp(record.created),
|
"message": record.getMessage(),
|
||||||
'level': record.levelname,
|
"time": datetime.datetime.fromtimestamp(record.created),
|
||||||
'file': format_fname(record.pathname),
|
"level": record.levelname,
|
||||||
'file_long': record.pathname,
|
"file": format_fname(record.pathname),
|
||||||
'line': record.lineno,
|
"file_long": record.pathname,
|
||||||
})
|
"line": record.lineno,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
context = self.context.copy()
|
context = self.context.copy()
|
||||||
context.update({'records': records})
|
context.update({"records": records})
|
||||||
|
return self.render("panels/logger.html", context)
|
||||||
return self.render('panels/logger.html', context)
|
|
||||||
|
|||||||
@@ -1,93 +1,121 @@
|
|||||||
|
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:
|
try:
|
||||||
import cProfile as profile
|
import cProfile as profile
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import profile
|
import profile # type: ignore[no-redef]
|
||||||
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):
|
class ProfilerDebugPanel(DebugPanel):
|
||||||
"""
|
"""Panel that displays the time a response took with cProfile output."""
|
||||||
Panel that displays the time a response took with cProfile output.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name = 'Profiler'
|
|
||||||
|
|
||||||
|
name = "Profiler"
|
||||||
user_activate = True
|
user_activate = True
|
||||||
|
|
||||||
def __init__(self, jinja_env, context={}):
|
is_active: bool = False
|
||||||
DebugPanel.__init__(self, jinja_env, context=context)
|
dump_filename: str | None = None
|
||||||
if current_app.config.get('DEBUG_TB_PROFILER_ENABLED'):
|
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)
|
||||||
|
|
||||||
|
self.dump_filename = None
|
||||||
|
|
||||||
|
if current_app.config.get("DEBUG_TB_PROFILER_ENABLED"):
|
||||||
self.is_active = True
|
self.is_active = True
|
||||||
self.dump_filename = current_app.config.get(
|
self.dump_filename = current_app.config.get(
|
||||||
"DEBUG_TB_PROFILER_DUMP_FILENAME"
|
"DEBUG_TB_PROFILER_DUMP_FILENAME"
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_content(self):
|
@property
|
||||||
|
def has_content(self) -> bool: # type: ignore[override]
|
||||||
return bool(self.profiler)
|
return bool(self.profiler)
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request: Request) -> None:
|
||||||
if not self.is_active:
|
if not self.is_active:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.profiler = profile.Profile()
|
self.profiler = profile.Profile() # pyright: ignore
|
||||||
self.stats = None
|
self.stats = None
|
||||||
|
|
||||||
def process_view(self, request, view_func, view_kwargs):
|
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:
|
if self.is_active:
|
||||||
func = functools.partial(self.profiler.runcall, view_func)
|
func = functools.partial(self.profiler.runcall, view_func)
|
||||||
functools.update_wrapper(func, view_func)
|
functools.update_wrapper(func, view_func)
|
||||||
return func
|
return func
|
||||||
|
|
||||||
def process_response(self, request, response):
|
return None
|
||||||
|
|
||||||
|
def process_response(self, request: Request, response: Response) -> None:
|
||||||
if not self.is_active:
|
if not self.is_active:
|
||||||
return False
|
return
|
||||||
|
|
||||||
if self.profiler is not None:
|
if self.profiler is not None:
|
||||||
self.profiler.disable()
|
self.profiler.disable() # pyright: ignore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stats = pstats.Stats(self.profiler)
|
stats = pstats.Stats(self.profiler)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
self.is_active = False
|
self.is_active = False
|
||||||
return False
|
return
|
||||||
function_calls = []
|
|
||||||
for func in stats.sort_stats(1).fcn_list:
|
function_calls: list[dict[str, t.Any]] = []
|
||||||
current = {}
|
|
||||||
info = stats.stats[func]
|
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
|
# Number of calls
|
||||||
if info[0] != info[1]:
|
if info[0] != info[1]:
|
||||||
current['ncalls'] = '%d/%d' % (info[1], info[0])
|
current["ncalls"] = f"{info[1]}/{info[0]}"
|
||||||
else:
|
else:
|
||||||
current['ncalls'] = info[1]
|
current["ncalls"] = info[1]
|
||||||
|
|
||||||
# Total time
|
# Total time
|
||||||
current['tottime'] = info[2] * 1000
|
current["tottime"] = info[2] * 1000
|
||||||
|
|
||||||
# Quotient of total time divided by number of calls
|
# Quotient of total time divided by number of calls
|
||||||
if info[1]:
|
if info[1]:
|
||||||
current['percall'] = info[2] * 1000 / info[1]
|
current["percall"] = info[2] * 1000 / info[1]
|
||||||
else:
|
else:
|
||||||
current['percall'] = 0
|
current["percall"] = 0
|
||||||
|
|
||||||
# Cumulative time
|
# Cumulative time
|
||||||
current['cumtime'] = info[3] * 1000
|
current["cumtime"] = info[3] * 1000
|
||||||
|
|
||||||
# Quotient of the cumulative time divided by the number of
|
# Quotient of the cumulative time divided by the number of
|
||||||
# primitive calls.
|
# primitive calls.
|
||||||
if info[0]:
|
if info[0]:
|
||||||
current['percall_cum'] = info[3] * 1000 / info[0]
|
current["percall_cum"] = info[3] * 1000 / info[0]
|
||||||
else:
|
else:
|
||||||
current['percall_cum'] = 0
|
current["percall_cum"] = 0
|
||||||
|
|
||||||
# Filename
|
# Filename
|
||||||
filename = pstats.func_std_string(func)
|
filename = pstats.func_std_string(func) # type: ignore[attr-defined]
|
||||||
current['filename_long'] = filename
|
current["filename_long"] = filename
|
||||||
current['filename'] = format_fname(filename)
|
current["filename"] = format_fname(filename)
|
||||||
function_calls.append(current)
|
function_calls.append(current)
|
||||||
|
|
||||||
self.stats = stats
|
self.stats = stats
|
||||||
@@ -98,32 +126,33 @@ class ProfilerDebugPanel(DebugPanel):
|
|||||||
filename = self.dump_filename()
|
filename = self.dump_filename()
|
||||||
else:
|
else:
|
||||||
filename = self.dump_filename
|
filename = self.dump_filename
|
||||||
|
|
||||||
self.profiler.dump_stats(filename)
|
self.profiler.dump_stats(filename)
|
||||||
|
|
||||||
return response
|
def title(self) -> str:
|
||||||
|
|
||||||
def title(self):
|
|
||||||
if not self.is_active:
|
if not self.is_active:
|
||||||
return "Profiler not active"
|
return "Profiler not active"
|
||||||
return 'View: %.2fms' % (float(self.stats.total_tt) * 1000,)
|
|
||||||
|
|
||||||
def nav_title(self):
|
return f"View: {float(self.stats.total_tt) * 1000:.2f}ms" # type: ignore[union-attr]
|
||||||
return 'Profiler'
|
|
||||||
|
|
||||||
def nav_subtitle(self):
|
def nav_title(self) -> str:
|
||||||
|
return "Profiler"
|
||||||
|
|
||||||
|
def nav_subtitle(self) -> str:
|
||||||
if not self.is_active:
|
if not self.is_active:
|
||||||
return "in-active"
|
return "in-active"
|
||||||
return 'View: %.2fms' % (float(self.stats.total_tt) * 1000,)
|
|
||||||
|
|
||||||
def url(self):
|
return f"View: {float(self.stats.total_tt) * 1000:.2f}ms" # type: ignore[union-attr]
|
||||||
return ''
|
|
||||||
|
|
||||||
def content(self):
|
def url(self) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def content(self) -> str:
|
||||||
if not self.is_active:
|
if not self.is_active:
|
||||||
return "The profiler is not activated, activate it to use it"
|
return "The profiler is not activated, activate it to use it"
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'stats': self.stats,
|
"stats": self.stats,
|
||||||
'function_calls': self.function_calls,
|
"function_calls": self.function_calls,
|
||||||
}
|
}
|
||||||
return self.render('panels/profiler.html', context)
|
return self.render("panels/profiler.html", context)
|
||||||
|
|||||||
@@ -1,49 +1,59 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections.abc as c
|
||||||
|
import typing as t
|
||||||
|
|
||||||
from flask import session
|
from flask import session
|
||||||
|
from werkzeug import Request
|
||||||
|
|
||||||
from flask_debugtoolbar.panels import DebugPanel
|
from . import DebugPanel
|
||||||
|
|
||||||
_ = lambda x: x
|
|
||||||
|
|
||||||
|
|
||||||
class RequestVarsDebugPanel(DebugPanel):
|
class RequestVarsDebugPanel(DebugPanel):
|
||||||
"""
|
"""A panel to display request variables (POST/GET, session, cookies)."""
|
||||||
A panel to display request variables (POST/GET, session, cookies).
|
|
||||||
"""
|
name = "RequestVars"
|
||||||
name = 'RequestVars'
|
|
||||||
has_content = True
|
has_content = True
|
||||||
|
|
||||||
def nav_title(self):
|
def nav_title(self) -> str:
|
||||||
return _('Request Vars')
|
return "Request Vars"
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
return _('Request Vars')
|
return "Request Vars"
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request: Request) -> None:
|
||||||
self.request = request
|
self.request = request
|
||||||
self.session = session
|
self.session = session
|
||||||
self.view_func = None
|
self.view_func: c.Callable[..., t.Any] | None = None
|
||||||
self.view_args = []
|
self.view_kwargs: dict[str, t.Any] = {}
|
||||||
self.view_kwargs = {}
|
|
||||||
|
|
||||||
def process_view(self, request, view_func, view_kwargs):
|
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_func = view_func
|
||||||
self.view_kwargs = view_kwargs
|
self.view_kwargs = view_kwargs
|
||||||
|
|
||||||
def content(self):
|
def content(self) -> str:
|
||||||
context = self.context.copy()
|
context = self.context.copy()
|
||||||
context.update({
|
context.update(
|
||||||
'get': self.request.args.lists(),
|
{
|
||||||
'post': self.request.form.lists(),
|
"get": self.request.args.lists(),
|
||||||
'cookies': self.request.cookies.items(),
|
"post": self.request.form.lists(),
|
||||||
'view_func': ('%s.%s' % (self.view_func.__module__,
|
"cookies": self.request.cookies.items(),
|
||||||
self.view_func.__name__)
|
"view_func": (
|
||||||
if self.view_func else '[unknown]'),
|
f"{self.view_func.__module__}.{self.view_func.__name__}"
|
||||||
'view_args': self.view_args,
|
if self.view_func
|
||||||
'view_kwargs': self.view_kwargs or {},
|
else "[unknown]"
|
||||||
'session': self.session.items(),
|
),
|
||||||
})
|
"view_kwargs": self.view_kwargs or {},
|
||||||
|
"session": self.session.items(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return self.render('panels/request_vars.html', context)
|
return self.render("panels/request_vars.html", context)
|
||||||
|
|||||||
@@ -1,38 +1,44 @@
|
|||||||
from flask_debugtoolbar.panels import DebugPanel
|
from __future__ import annotations
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
_ = lambda x: x
|
from flask import current_app
|
||||||
|
from werkzeug import Request
|
||||||
|
from werkzeug.routing import Rule
|
||||||
|
|
||||||
|
from . import DebugPanel
|
||||||
|
|
||||||
|
|
||||||
class RouteListDebugPanel(DebugPanel):
|
class RouteListDebugPanel(DebugPanel):
|
||||||
"""
|
"""Panel that displays the URL routing rules."""
|
||||||
Panel that displays the URL routing rules.
|
|
||||||
"""
|
name = "RouteList"
|
||||||
name = 'RouteList'
|
|
||||||
has_content = True
|
has_content = True
|
||||||
routes = []
|
routes: list[Rule] = []
|
||||||
|
|
||||||
def nav_title(self):
|
def nav_title(self) -> str:
|
||||||
return _('Route List')
|
return "Route List"
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
return _('Route List')
|
return "Route List"
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def nav_subtitle(self):
|
def nav_subtitle(self) -> str:
|
||||||
count = len(self.routes)
|
count = len(self.routes)
|
||||||
return '%s %s' % (count, 'route' if count == 1 else 'routes')
|
plural = "route" if count == 1 else "routes"
|
||||||
|
return f"{count} {plural}"
|
||||||
|
|
||||||
def process_request(self, request):
|
def process_request(self, request: Request) -> None:
|
||||||
self.routes = [
|
self.routes = [
|
||||||
rule
|
rule
|
||||||
for rule in current_app.url_map.iter_rules()
|
for rule in current_app.url_map.iter_rules()
|
||||||
if not rule.rule.startswith('/_debug_toolbar')
|
if not rule.rule.startswith("/_debug_toolbar")
|
||||||
]
|
]
|
||||||
|
|
||||||
def content(self):
|
def content(self) -> str:
|
||||||
return self.render('panels/route_list.html', {
|
return self.render(
|
||||||
'routes': self.routes,
|
"panels/route_list.html",
|
||||||
})
|
{
|
||||||
|
"routes": self.routes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,46 +1,63 @@
|
|||||||
|
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:
|
try:
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
except ImportError:
|
except ImportError:
|
||||||
sqlalchemy_available = False
|
sqlalchemy_available: bool = False
|
||||||
get_recorded_queries = SQLAlchemy = None
|
get_recorded_queries = SQLAlchemy = None # type: ignore[misc, assignment]
|
||||||
debug_enables_record_queries = False
|
debug_enables_record_queries: bool = False
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
from flask_sqlalchemy.record_queries import get_recorded_queries
|
from flask_sqlalchemy.record_queries import ( # type: ignore[assignment]
|
||||||
|
get_recorded_queries,
|
||||||
|
)
|
||||||
|
|
||||||
debug_enables_record_queries = False
|
debug_enables_record_queries = False
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# For flask_sqlalchemy < 3.0.0
|
# For flask_sqlalchemy < 3.0.0
|
||||||
from flask_sqlalchemy import get_debug_queries as get_recorded_queries
|
from flask_sqlalchemy import ( # type: ignore[no-redef]
|
||||||
|
get_debug_queries as get_recorded_queries,
|
||||||
|
)
|
||||||
|
|
||||||
# flask_sqlalchemy < 3.0.0 automatically enabled
|
# flask_sqlalchemy < 3.0.0 automatically enabled
|
||||||
# SQLALCHEMY_RECORD_QUERIES in debug or test mode
|
# SQLALCHEMY_RECORD_QUERIES in debug or test mode
|
||||||
debug_enables_record_queries = True
|
debug_enables_record_queries = True
|
||||||
|
location_property: str = "context"
|
||||||
location_property = 'context'
|
|
||||||
else:
|
else:
|
||||||
location_property = 'location'
|
location_property = "location"
|
||||||
|
|
||||||
sqlalchemy_available = True
|
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() -> itsdangerous.URLSafeSerializer:
|
||||||
|
return itsdangerous.URLSafeSerializer(
|
||||||
|
current_app.config["SECRET_KEY"], salt="fdt-sql-query"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def query_signer():
|
def is_select(statement: str | bytes) -> bool:
|
||||||
return itsdangerous.URLSafeSerializer(current_app.config['SECRET_KEY'],
|
statement = statement.lower().strip()
|
||||||
salt='fdt-sql-query')
|
|
||||||
|
if isinstance(statement, bytes):
|
||||||
|
return statement.startswith(b"select")
|
||||||
|
|
||||||
|
return statement.startswith("select") # pyright: ignore
|
||||||
|
|
||||||
|
|
||||||
def is_select(statement):
|
def dump_query(statement: str, params: t.Any) -> str | None:
|
||||||
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):
|
if not params or not is_select(statement):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -50,9 +67,9 @@ def dump_query(statement, params):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def load_query(data):
|
def load_query(data: str) -> tuple[str, t.Any]:
|
||||||
try:
|
try:
|
||||||
statement, params = query_signer().loads(request.args['query'])
|
statement, params = query_signer().loads(data)
|
||||||
except (itsdangerous.BadSignature, TypeError):
|
except (itsdangerous.BadSignature, TypeError):
|
||||||
abort(406)
|
abort(406)
|
||||||
|
|
||||||
@@ -63,22 +80,21 @@ def load_query(data):
|
|||||||
return statement, params
|
return statement, params
|
||||||
|
|
||||||
|
|
||||||
def extension_used():
|
def extension_used() -> bool:
|
||||||
return 'sqlalchemy' in current_app.extensions
|
return "sqlalchemy" in current_app.extensions
|
||||||
|
|
||||||
|
|
||||||
def recording_enabled():
|
def recording_enabled() -> bool:
|
||||||
return (
|
return (
|
||||||
(debug_enables_record_queries and current_app.debug) or
|
debug_enables_record_queries and current_app.debug
|
||||||
current_app.config.get('SQLALCHEMY_RECORD_QUERIES')
|
) or current_app.config.get("SQLALCHEMY_RECORD_QUERIES", False)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def is_available():
|
def is_available() -> bool:
|
||||||
return sqlalchemy_available and extension_used() and recording_enabled()
|
return sqlalchemy_available and extension_used() and recording_enabled()
|
||||||
|
|
||||||
|
|
||||||
def get_queries():
|
def get_queries() -> list[t.Any]:
|
||||||
if get_recorded_queries:
|
if get_recorded_queries:
|
||||||
return get_recorded_queries()
|
return get_recorded_queries()
|
||||||
else:
|
else:
|
||||||
@@ -86,80 +102,85 @@ def get_queries():
|
|||||||
|
|
||||||
|
|
||||||
class SQLAlchemyDebugPanel(DebugPanel):
|
class SQLAlchemyDebugPanel(DebugPanel):
|
||||||
"""
|
"""Panel that displays the time a response took in milliseconds."""
|
||||||
Panel that displays the time a response took in milliseconds.
|
|
||||||
"""
|
name = "SQLAlchemy"
|
||||||
name = 'SQLAlchemy'
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_content(self):
|
def has_content(self) -> bool: # type: ignore[override]
|
||||||
return bool(get_queries()) or not is_available()
|
return bool(get_queries()) or not is_available()
|
||||||
|
|
||||||
def process_request(self, request):
|
def nav_title(self) -> str:
|
||||||
pass
|
return "SQLAlchemy"
|
||||||
|
|
||||||
def process_response(self, request, response):
|
def nav_subtitle(self) -> str:
|
||||||
pass
|
|
||||||
|
|
||||||
def nav_title(self):
|
|
||||||
return _('SQLAlchemy')
|
|
||||||
|
|
||||||
def nav_subtitle(self):
|
|
||||||
count = len(get_queries())
|
count = len(get_queries())
|
||||||
|
|
||||||
if not count and not is_available():
|
if not count and not is_available():
|
||||||
return 'Unavailable'
|
return "Unavailable"
|
||||||
|
|
||||||
return '%d %s' % (count, 'query' if count == 1 else 'queries')
|
plural = "query" if count == 1 else "queries"
|
||||||
|
return f"{count} {plural}"
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
return _('SQLAlchemy queries')
|
return "SQLAlchemy queries"
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def content(self):
|
def content(self) -> str:
|
||||||
queries = get_queries()
|
queries = get_queries()
|
||||||
|
|
||||||
if not queries and not is_available():
|
if not queries and not is_available():
|
||||||
return self.render('panels/sqlalchemy_error.html', {
|
return self.render(
|
||||||
'sqlalchemy_available': sqlalchemy_available,
|
"panels/sqlalchemy_error.html",
|
||||||
'extension_used': extension_used(),
|
{
|
||||||
'recording_enabled': recording_enabled(),
|
"sqlalchemy_available": sqlalchemy_available,
|
||||||
})
|
"extension_used": extension_used(),
|
||||||
|
"recording_enabled": recording_enabled(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for query in queries:
|
for query in queries:
|
||||||
data.append({
|
data.append(
|
||||||
'duration': query.duration,
|
{
|
||||||
'sql': format_sql(query.statement, query.parameters),
|
"duration": query.duration,
|
||||||
'signed_query': dump_query(query.statement, query.parameters),
|
"sql": format_sql(query.statement, query.parameters),
|
||||||
'location_long': getattr(query, location_property),
|
"signed_query": dump_query(query.statement, query.parameters),
|
||||||
'location': format_fname(getattr(query, location_property))
|
"location_long": getattr(query, location_property),
|
||||||
})
|
"location": format_fname(getattr(query, location_property)),
|
||||||
return self.render('panels/sqlalchemy.html', {'queries': data})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.render("panels/sqlalchemy.html", {"queries": data})
|
||||||
|
|
||||||
|
|
||||||
# Panel views
|
# Panel views
|
||||||
|
|
||||||
|
|
||||||
@module.route('/sqlalchemy/sql_select', methods=['GET', 'POST'])
|
@module.route("/sqlalchemy/sql_select", methods=["GET", "POST"])
|
||||||
@module.route('/sqlalchemy/sql_explain', methods=['GET', 'POST'],
|
@module.route(
|
||||||
defaults=dict(explain=True))
|
"/sqlalchemy/sql_explain", methods=["GET", "POST"], defaults=dict(explain=True)
|
||||||
def sql_select(explain=False):
|
)
|
||||||
statement, params = load_query(request.args['query'])
|
def sql_select(explain: bool = False) -> str:
|
||||||
engine = SQLAlchemy().get_engine(current_app)
|
statement, params = load_query(request.args["query"])
|
||||||
|
engine = current_app.extensions["sqlalchemy"].engine
|
||||||
|
|
||||||
if explain:
|
if explain:
|
||||||
if engine.driver == 'pysqlite':
|
if engine.driver == "pysqlite":
|
||||||
statement = 'EXPLAIN QUERY PLAN\n%s' % statement
|
statement = f"EXPLAIN QUERY PLAN\n{statement}"
|
||||||
else:
|
else:
|
||||||
statement = 'EXPLAIN\n%s' % statement
|
statement = f"EXPLAIN\n{statement}"
|
||||||
|
|
||||||
result = engine.execute(statement, params)
|
result = engine.execute(statement, params)
|
||||||
return g.debug_toolbar.render('panels/sqlalchemy_select.html', {
|
return g.debug_toolbar.render( # type: ignore[no-any-return]
|
||||||
'result': result.fetchall(),
|
"panels/sqlalchemy_select.html",
|
||||||
'headers': result.keys(),
|
{
|
||||||
'sql': format_sql(statement, params),
|
"result": result.fetchall(),
|
||||||
'duration': float(request.args['duration']),
|
"headers": result.keys(),
|
||||||
})
|
"sql": format_sql(statement, params),
|
||||||
|
"duration": float(request.args["duration"]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,136 +1,149 @@
|
|||||||
import collections
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import typing as t
|
||||||
import uuid
|
import uuid
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
from flask import (
|
from flask import abort
|
||||||
template_rendered, request, g,
|
from flask import current_app
|
||||||
Response, current_app, abort, url_for
|
from flask import g
|
||||||
)
|
from flask import request
|
||||||
from flask_debugtoolbar import module
|
from flask import Response
|
||||||
from flask_debugtoolbar.panels import DebugPanel
|
from flask import template_rendered
|
||||||
|
from flask import url_for
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
_ = lambda x: x
|
from .. import module
|
||||||
|
from . import DebugPanel
|
||||||
|
|
||||||
|
|
||||||
class TemplateDebugPanel(DebugPanel):
|
class TemplateDebugPanel(DebugPanel):
|
||||||
"""
|
"""Panel that displays the time a response took in milliseconds."""
|
||||||
Panel that displays the time a response took in milliseconds.
|
|
||||||
"""
|
name = "Template"
|
||||||
name = 'Template'
|
|
||||||
has_content = True
|
has_content = True
|
||||||
|
|
||||||
# save the context for the 5 most recent requests
|
# save the context for the 5 most recent requests
|
||||||
template_cache = collections.deque(maxlen=5)
|
template_cache: deque[tuple[str, list[dict[str, t.Any]]]] = deque(maxlen=5)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_cache_for_key(self, key):
|
def get_cache_for_key(cls, key: str) -> list[dict[str, t.Any]]:
|
||||||
for cache_key, value in self.template_cache:
|
for cache_key, value in cls.template_cache:
|
||||||
if key == cache_key:
|
if key == cache_key:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
||||||
super(self.__class__, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.key = str(uuid.uuid4())
|
self.key: str = str(uuid.uuid4())
|
||||||
self.templates = []
|
self.templates: list[dict[str, t.Any]] = []
|
||||||
template_rendered.connect(self._store_template_info)
|
template_rendered.connect(self._store_template_info)
|
||||||
|
|
||||||
def _store_template_info(self, sender, **kwargs):
|
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
|
# only record in the cache if the editor is enabled and there is
|
||||||
# actually a template for this request
|
# actually a template for this request
|
||||||
if not self.templates and is_editor_enabled():
|
if not self.templates and is_editor_enabled():
|
||||||
self.template_cache.append((self.key, self.templates))
|
self.template_cache.append((self.key, self.templates))
|
||||||
|
|
||||||
self.templates.append(kwargs)
|
self.templates.append(kwargs)
|
||||||
|
|
||||||
def process_request(self, request):
|
def nav_title(self) -> str:
|
||||||
pass
|
return "Templates"
|
||||||
|
|
||||||
def process_response(self, request, response):
|
def nav_subtitle(self) -> str:
|
||||||
pass
|
return f"{len(self.templates)} rendered"
|
||||||
|
|
||||||
def nav_title(self):
|
def title(self) -> str:
|
||||||
return _('Templates')
|
return "Templates"
|
||||||
|
|
||||||
def nav_subtitle(self):
|
def url(self) -> str:
|
||||||
return "%d rendered" % len(self.templates)
|
return ""
|
||||||
|
|
||||||
def title(self):
|
def content(self) -> str:
|
||||||
return _('Templates')
|
return self.render(
|
||||||
|
"panels/template.html",
|
||||||
def url(self):
|
{
|
||||||
return ''
|
"key": self.key,
|
||||||
|
"templates": self.templates,
|
||||||
def content(self):
|
"editable": is_editor_enabled(),
|
||||||
return self.render('panels/template.html', {
|
},
|
||||||
'key': self.key,
|
)
|
||||||
'templates': self.templates,
|
|
||||||
'editable': is_editor_enabled(),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def is_editor_enabled():
|
def is_editor_enabled() -> bool:
|
||||||
return current_app.config.get('DEBUG_TB_TEMPLATE_EDITOR_ENABLED')
|
return current_app.config.get("DEBUG_TB_TEMPLATE_EDITOR_ENABLED", False) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def require_enabled():
|
def require_enabled() -> None:
|
||||||
if not is_editor_enabled():
|
if not is_editor_enabled():
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
def _get_source(template):
|
def _get_source(template: Template) -> str:
|
||||||
with open(template.filename, 'rb') as fp:
|
if template.filename is None:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
with open(template.filename, "rb") as fp:
|
||||||
source = fp.read()
|
source = fp.read()
|
||||||
|
|
||||||
return source.decode(_template_encoding())
|
return source.decode(_template_encoding())
|
||||||
|
|
||||||
|
|
||||||
def _template_encoding():
|
def _template_encoding() -> str:
|
||||||
return getattr(current_app.jinja_loader, 'encoding', 'utf-8')
|
return getattr(current_app.jinja_loader, "encoding", "utf-8")
|
||||||
|
|
||||||
|
|
||||||
@module.route('/template/<key>')
|
@module.route("/template/<key>")
|
||||||
def template_editor(key):
|
def template_editor(key: str) -> str:
|
||||||
require_enabled()
|
require_enabled()
|
||||||
# TODO set up special loader that caches templates it loads
|
# TODO set up special loader that caches templates it loads
|
||||||
# and can override template contents
|
# and can override template contents
|
||||||
templates = [t['template'] for t in
|
templates = [t["template"] for t in TemplateDebugPanel.get_cache_for_key(key)]
|
||||||
TemplateDebugPanel.get_cache_for_key(key)]
|
return g.debug_toolbar.render( # type: ignore[no-any-return]
|
||||||
return g.debug_toolbar.render('panels/template_editor.html', {
|
"panels/template_editor.html",
|
||||||
'static_path': url_for('_debug_toolbar.static', filename=''),
|
{
|
||||||
'request': request,
|
"static_path": url_for("_debug_toolbar.static", filename=""),
|
||||||
'templates': [
|
"request": request,
|
||||||
{'name': t.name, 'source': _get_source(t)}
|
"templates": [
|
||||||
for t in templates
|
{"name": t.name, "source": _get_source(t)} for t in templates
|
||||||
]
|
],
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@module.route('/template/<key>/save', methods=['POST'])
|
@module.route("/template/<key>/save", methods=["POST"])
|
||||||
def save_template(key):
|
def save_template(key: str) -> str:
|
||||||
require_enabled()
|
require_enabled()
|
||||||
template = TemplateDebugPanel.get_cache_for_key(key)[0]['template']
|
template = TemplateDebugPanel.get_cache_for_key(key)[0]["template"]
|
||||||
content = request.form['content'].encode(_template_encoding())
|
content = request.form["content"].encode(_template_encoding())
|
||||||
with open(template.filename, 'wb') as fp:
|
|
||||||
|
with open(template.filename, "wb") as fp:
|
||||||
fp.write(content)
|
fp.write(content)
|
||||||
return 'ok'
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
@module.route('/template/<key>', methods=['POST'])
|
@module.route("/template/<key>", methods=["POST"])
|
||||||
def template_preview(key):
|
def template_preview(key: str) -> str | Response:
|
||||||
require_enabled()
|
require_enabled()
|
||||||
context = TemplateDebugPanel.get_cache_for_key(key)[0]['context']
|
context = TemplateDebugPanel.get_cache_for_key(key)[0]["context"]
|
||||||
content = request.form['content']
|
content = request.form["content"]
|
||||||
env = current_app.jinja_env.overlay(autoescape=True)
|
env = current_app.jinja_env.overlay(autoescape=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
template = env.from_string(content)
|
template = env.from_string(content)
|
||||||
return template.render(context)
|
return template.render(context)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tb = sys.exc_info()[2]
|
tb = sys.exc_info()[2]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while tb.tb_next:
|
while tb.tb_next: # type: ignore[union-attr]
|
||||||
tb = tb.tb_next
|
tb = tb.tb_next # type: ignore[union-attr]
|
||||||
msg = {'lineno': tb.tb_lineno, 'error': str(e)}
|
|
||||||
return Response(json.dumps(msg), status=400,
|
msg = {"lineno": tb.tb_lineno, "error": str(e)} # type: ignore[union-attr]
|
||||||
mimetype='application/json')
|
return Response(json.dumps(msg), status=400, mimetype="application/json")
|
||||||
finally:
|
finally:
|
||||||
del tb
|
del tb
|
||||||
|
|||||||
@@ -1,95 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
from werkzeug import Request
|
||||||
|
from werkzeug import Response
|
||||||
|
|
||||||
|
from . import DebugPanel
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import resource
|
import resource
|
||||||
except ImportError:
|
|
||||||
pass # Will fail on Win32 systems
|
|
||||||
import time
|
|
||||||
from flask_debugtoolbar.panels import DebugPanel
|
|
||||||
|
|
||||||
_ = lambda x: x
|
HAVE_RESOURCE = True
|
||||||
|
except ImportError:
|
||||||
|
HAVE_RESOURCE = False
|
||||||
|
|
||||||
|
|
||||||
class TimerDebugPanel(DebugPanel):
|
class TimerDebugPanel(DebugPanel):
|
||||||
"""
|
"""Panel that displays the time a response took in milliseconds."""
|
||||||
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):
|
name = "Timer"
|
||||||
|
has_content = HAVE_RESOURCE
|
||||||
|
|
||||||
|
def process_request(self, request: Request) -> None:
|
||||||
self._start_time = time.time()
|
self._start_time = time.time()
|
||||||
if self.has_resource:
|
|
||||||
|
if HAVE_RESOURCE:
|
||||||
self._start_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
self._start_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||||
|
|
||||||
def process_response(self, request, response):
|
def process_response(self, request: Request, response: Response) -> None:
|
||||||
self.total_time = (time.time() - self._start_time) * 1000
|
self.total_time: float = (time.time() - self._start_time) * 1000
|
||||||
if self.has_resource:
|
|
||||||
|
if HAVE_RESOURCE:
|
||||||
self._end_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
self._end_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||||
|
|
||||||
def nav_title(self):
|
def nav_title(self) -> str:
|
||||||
return _('Time')
|
return "Time"
|
||||||
|
|
||||||
def nav_subtitle(self):
|
def nav_subtitle(self) -> str:
|
||||||
# TODO l10n
|
if not HAVE_RESOURCE:
|
||||||
if not self.has_resource:
|
return f"TOTAL: {self.total_time:0.2f}ms"
|
||||||
return 'TOTAL: %0.2fms' % (self.total_time)
|
|
||||||
|
|
||||||
utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime
|
utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime
|
||||||
stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime
|
stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime
|
||||||
return 'CPU: %0.2fms (%0.2fms)' % (
|
return f"CPU: {(utime + stime) * 1000.0:0.2f}ms ({self.total_time:0.2f}ms)"
|
||||||
(utime + stime) * 1000.0, self.total_time)
|
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
return _('Resource Usage')
|
return "Resource Usage"
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def _elapsed_ru(self, name):
|
def _elapsed_ru(self, name: str) -> float:
|
||||||
return (getattr(self._end_rusage, name) - getattr(self._start_rusage, name))
|
return getattr(self._end_rusage, name) - getattr(self._start_rusage, name) # type: ignore[no-any-return]
|
||||||
|
|
||||||
def content(self):
|
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")
|
||||||
|
|
||||||
utime = 1000 * self._elapsed_ru('ru_utime')
|
# these are documented as not meaningful under Linux. If you're running BSD
|
||||||
stime = 1000 * self._elapsed_ru('ru_stime')
|
# feel free to enable them, and add any others that I hadn't gotten to before
|
||||||
vcsw = self._elapsed_ru('ru_nvcsw')
|
# I noticed that I was getting nothing but zeroes and that the docs agreed. :-(
|
||||||
ivcsw = self._elapsed_ru('ru_nivcsw')
|
# blkin = self._elapsed_ru("ru_inblock")
|
||||||
# minflt = self._elapsed_ru('ru_minflt')
|
# blkout = self._elapsed_ru("ru_oublock")
|
||||||
# majflt = self._elapsed_ru('ru_majflt')
|
# swap = self._elapsed_ru("ru_nswap")
|
||||||
|
# rss = self._end_rusage.ru_maxrss
|
||||||
# these are documented as not meaningful under Linux. If you're running BSD
|
# srss = self._end_rusage.ru_ixrss
|
||||||
# feel free to enable them, and add any others that I hadn't gotten to before
|
# urss = self._end_rusage.ru_idrss
|
||||||
# I noticed that I was getting nothing but zeroes and that the docs agreed. :-(
|
# usrss = self._end_rusage.ru_isrss
|
||||||
#
|
|
||||||
# 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 = (
|
rows = (
|
||||||
(_('User CPU time'), '%0.3f msec' % utime),
|
("User CPU time", f"{utime:0.3f} msec"),
|
||||||
(_('System CPU time'), '%0.3f msec' % stime),
|
("System CPU time", f"{stime:0.3f} msec"),
|
||||||
(_('Total CPU time'), '%0.3f msec' % (utime + stime)),
|
("Total CPU time", f"{(utime + stime):0.3f} msec"),
|
||||||
(_('Elapsed time'), '%0.3f msec' % self.total_time),
|
("Elapsed time", f"{self.total_time:0.3f} msec"),
|
||||||
(_('Context switches'), '%d voluntary, %d involuntary' % (vcsw, ivcsw)),
|
("Context switches", f"{vcsw} voluntary, {ivcsw} involuntary"),
|
||||||
# ('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)),
|
# "Memory use",
|
||||||
# ('Disk operations', '%d in, %d out, %d swapout' % (blkin, blkout, swap)),
|
# 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 = self.context.copy()
|
||||||
context.update({
|
context.update(
|
||||||
'rows': rows,
|
{
|
||||||
})
|
"rows": rows,
|
||||||
|
}
|
||||||
return self.render('panels/timer.html', context)
|
)
|
||||||
|
return self.render("panels/timer.html", context)
|
||||||
|
|||||||
@@ -1,51 +1,39 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.metadata
|
||||||
import os
|
import os
|
||||||
from sysconfig import get_path
|
from sysconfig import get_path
|
||||||
|
|
||||||
from flask_debugtoolbar.panels import DebugPanel
|
from . import DebugPanel
|
||||||
|
|
||||||
try:
|
flask_version: str = importlib.metadata.version("flask")
|
||||||
# Python 3.8+
|
|
||||||
from importlib.metadata import version
|
|
||||||
|
|
||||||
flask_version = version('flask')
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
import pkg_resources
|
|
||||||
|
|
||||||
flask_version = pkg_resources.get_distribution('flask').version
|
|
||||||
|
|
||||||
_ = lambda x: x
|
|
||||||
|
|
||||||
|
|
||||||
class VersionDebugPanel(DebugPanel):
|
class VersionDebugPanel(DebugPanel):
|
||||||
"""
|
"""Panel that displays the Flask version."""
|
||||||
Panel that displays the Flask version.
|
|
||||||
"""
|
name = "Version"
|
||||||
name = 'Version'
|
|
||||||
has_content = True
|
has_content = True
|
||||||
|
|
||||||
def nav_title(self):
|
def nav_title(self) -> str:
|
||||||
return _('Versions')
|
return "Versions"
|
||||||
|
|
||||||
def nav_subtitle(self):
|
def nav_subtitle(self) -> str:
|
||||||
return 'Flask %s' % flask_version
|
return f"Flask {flask_version}"
|
||||||
|
|
||||||
def url(self):
|
def url(self) -> str:
|
||||||
return ''
|
return ""
|
||||||
|
|
||||||
def title(self):
|
def title(self) -> str:
|
||||||
return _('Versions')
|
return "Versions"
|
||||||
|
|
||||||
def content(self):
|
def content(self) -> str:
|
||||||
try:
|
packages_metadata = [p.metadata for p in importlib.metadata.distributions()]
|
||||||
import importlib.metadata
|
packages = sorted(packages_metadata, key=lambda p: p["Name"].lower())
|
||||||
except ImportError:
|
return self.render(
|
||||||
packages = []
|
"panels/versions.html",
|
||||||
else:
|
{
|
||||||
packages_metadata = [p.metadata for p in importlib.metadata.distributions()]
|
"packages": packages,
|
||||||
packages = sorted(packages_metadata, key=lambda p: p['Name'].lower())
|
"python_lib_dir": os.path.normpath(get_path("platlib")),
|
||||||
|
},
|
||||||
return self.render('panels/versions.html', {
|
)
|
||||||
'packages': packages,
|
|
||||||
'python_lib_dir': os.path.normpath(get_path('platlib')),
|
|
||||||
})
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
(function($) {
|
(function($) {
|
||||||
$.cookie = function(name, value, options) { if (typeof value != 'undefined') { options = options || {}; if (value === null) { value = ''; options.expires = -1; } var expires = ''; if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { var date; if (typeof options.expires == 'number') { date = new Date(); date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); } else { date = options.expires; } expires = '; expires=' + date.toUTCString(); } var path = options.path ? '; path=' + (options.path) : ''; var domain = options.domain ? '; domain=' + (options.domain) : ''; var secure = options.secure ? '; secure' : ''; document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); } else { var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = $.trim(cookies[i]); if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } };
|
$.cookie = function(name, value, options) { if (typeof value != 'undefined') { options = options || {}; if (value === null) { value = ''; options.expires = -1; } var expires = ''; if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { var date; if (typeof options.expires == 'number') { date = new Date(); date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); } else { date = options.expires; } expires = '; expires=' + date.toUTCString(); } var path = options.path ? '; path=' + (options.path) : ''; var domain = options.domain ? '; domain=' + (options.domain) : ''; var secure = options.secure ? '; secure' : ''; document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); } else { var cookieValue = null; if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = cookies[i].trim(); if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); break; } } } return cookieValue; } };
|
||||||
$('head').append('<link rel="stylesheet" href="'+DEBUG_TOOLBAR_STATIC_PATH+'css/toolbar.css?'+ Math.random() +'" type="text/css" />');
|
$('head').append('<link rel="stylesheet" href="'+DEBUG_TOOLBAR_STATIC_PATH+'css/toolbar.css?'+ Math.random() +'" type="text/css" />');
|
||||||
var COOKIE_NAME = 'fldt';
|
var COOKIE_NAME = 'fldt';
|
||||||
var COOKIE_NAME_ACTIVE = COOKIE_NAME +'_active';
|
var COOKIE_NAME_ACTIVE = COOKIE_NAME +'_active';
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
});
|
});
|
||||||
$(this).tablesorter({headers: headers});
|
$(this).tablesorter({headers: headers});
|
||||||
})
|
})
|
||||||
.bind('sortEnd', function() {
|
.on('sortEnd', function() {
|
||||||
$(this).find('tbody tr').each(function(idx, elem) {
|
$(this).find('tbody tr').each(function(idx, elem) {
|
||||||
var even = idx % 2 === 0;
|
var even = idx % 2 === 0;
|
||||||
$(elem)
|
$(elem)
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
$('#flDebugToolbar').hide('fast');
|
$('#flDebugToolbar').hide('fast');
|
||||||
$('#flDebugToolbarHandle').show();
|
$('#flDebugToolbarHandle').show();
|
||||||
// Unbind keydown
|
// Unbind keydown
|
||||||
$(document).unbind('keydown.flDebug');
|
$(document).off('keydown.flDebug');
|
||||||
if (setCookie) {
|
if (setCookie) {
|
||||||
$.cookie(COOKIE_NAME, 'hide', {
|
$.cookie(COOKIE_NAME, 'hide', {
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
@@ -4,14 +4,12 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>View Function</th>
|
<th>View Function</th>
|
||||||
<th>args</th>
|
|
||||||
<th>kwargs</th>
|
<th>kwargs</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ view_func }}</td>
|
<td>{{ view_func }}</td>
|
||||||
<td>{{ view_args|default("None") }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{% if view_kwargs.items() %}
|
{% if view_kwargs.items() %}
|
||||||
{% for k, v in view_kwargs.items() %}
|
{% for k, v in view_kwargs.items() %}
|
||||||
|
|||||||
@@ -24,12 +24,6 @@
|
|||||||
<td>{{ package.get('Home-page') }}</td>
|
<td>{{ package.get('Home-page') }}</td>
|
||||||
<td>{{ package.get('Summary') }}</td>
|
<td>{{ package.get('Summary') }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td>Python 3.8</td>
|
|
||||||
<td>NOT INSTALLED</td>
|
|
||||||
<td>This panel requires Python >= 3.8 in order to display installed packages and version information.</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,65 +1,68 @@
|
|||||||
try:
|
from __future__ import annotations
|
||||||
from urllib.parse import unquote
|
|
||||||
except ImportError:
|
|
||||||
from urllib import unquote
|
|
||||||
|
|
||||||
from flask import url_for, current_app
|
import collections.abc as c
|
||||||
|
import typing as t
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
from flask import current_app
|
||||||
|
from flask import Flask
|
||||||
|
from flask import url_for
|
||||||
|
from jinja2 import Environment
|
||||||
|
from werkzeug import Request
|
||||||
from werkzeug.utils import import_string
|
from werkzeug.utils import import_string
|
||||||
|
|
||||||
|
from .panels import DebugPanel
|
||||||
|
|
||||||
class DebugToolbar(object):
|
|
||||||
|
|
||||||
_cached_panel_classes = {}
|
class DebugToolbar:
|
||||||
|
_cached_panel_classes: t.ClassVar[dict[str, type[DebugPanel] | None]] = {}
|
||||||
|
|
||||||
def __init__(self, request, jinja_env):
|
def __init__(self, request: Request, jinja_env: Environment) -> None:
|
||||||
self.jinja_env = jinja_env
|
self.jinja_env = jinja_env
|
||||||
self.request = request
|
self.request = request
|
||||||
self.panels = []
|
self.panels: list[DebugPanel] = []
|
||||||
|
self.template_context: dict[str, t.Any] = {
|
||||||
self.template_context = {
|
"static_path": url_for("_debug_toolbar.static", filename="")
|
||||||
'static_path': url_for('_debug_toolbar.static', filename='')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.create_panels()
|
self.create_panels()
|
||||||
|
|
||||||
def create_panels(self):
|
def create_panels(self) -> None:
|
||||||
"""
|
"""Populate debug panels"""
|
||||||
Populate debug panels
|
activated_str = self.request.cookies.get("fldt_active", "")
|
||||||
"""
|
activated = unquote(activated_str).split(";")
|
||||||
activated = self.request.cookies.get('fldt_active', '')
|
|
||||||
activated = unquote(activated).split(';')
|
|
||||||
|
|
||||||
for panel_class in self._iter_panels(current_app):
|
for panel_class in self._iter_panels(current_app):
|
||||||
panel_instance = panel_class(jinja_env=self.jinja_env,
|
panel_instance = panel_class(
|
||||||
context=self.template_context)
|
jinja_env=self.jinja_env, context=self.template_context
|
||||||
|
)
|
||||||
|
|
||||||
if panel_instance.dom_id() in activated:
|
if panel_instance.dom_id() in activated:
|
||||||
panel_instance.is_active = True
|
panel_instance.is_active = True
|
||||||
|
|
||||||
self.panels.append(panel_instance)
|
self.panels.append(panel_instance)
|
||||||
|
|
||||||
def render_toolbar(self):
|
def render_toolbar(self) -> str:
|
||||||
context = self.template_context.copy()
|
context = self.template_context.copy()
|
||||||
context.update({'panels': self.panels})
|
context.update({"panels": self.panels})
|
||||||
|
template = self.jinja_env.get_template("base.html")
|
||||||
template = self.jinja_env.get_template('base.html')
|
|
||||||
return template.render(**context)
|
return template.render(**context)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_panels(cls, app):
|
def load_panels(cls, app: Flask) -> None:
|
||||||
for panel_class in cls._iter_panels(app):
|
for panel_class in cls._iter_panels(app):
|
||||||
# Call `.init_app()` on panels
|
# Call `.init_app()` on panels
|
||||||
panel_class.init_app(app)
|
panel_class.init_app(app)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _iter_panels(cls, app):
|
def _iter_panels(cls, app: Flask) -> c.Iterator[type[DebugPanel]]:
|
||||||
for panel_path in app.config['DEBUG_TB_PANELS']:
|
for panel_path in app.config["DEBUG_TB_PANELS"]:
|
||||||
panel_class = cls._import_panel(app, panel_path)
|
panel_class = cls._import_panel(app, panel_path)
|
||||||
|
|
||||||
if panel_class is not None:
|
if panel_class is not None:
|
||||||
yield panel_class
|
yield panel_class
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _import_panel(cls, app, path):
|
def _import_panel(cls, app: Flask, path: str) -> type[DebugPanel] | None:
|
||||||
cache = cls._cached_panel_classes
|
cache = cls._cached_panel_classes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -68,9 +71,9 @@ class DebugToolbar(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
panel_class = import_string(path)
|
panel_class: type[DebugPanel] | None = import_string(path)
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
app.logger.warning('Disabled %s due to ImportError: %s', path, e)
|
app.logger.warning("Disabled %s due to ImportError: %s", path, e)
|
||||||
panel_class = None
|
panel_class = None
|
||||||
|
|
||||||
cache[path] = panel_class
|
cache[path] = panel_class
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import collections.abc as c
|
||||||
|
import gzip
|
||||||
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
import io
|
from types import ModuleType
|
||||||
import gzip
|
|
||||||
|
from flask import current_app
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
from pygments.formatters import HtmlFormatter
|
from pygments.formatters import HtmlFormatter
|
||||||
from pygments.lexers import SqlLexer
|
from pygments.lexers import SqlLexer
|
||||||
from pygments.styles import get_style_by_name
|
from pygments.styles import get_style_by_name
|
||||||
PYGMENT_STYLE = get_style_by_name('colorful')
|
|
||||||
|
PYGMENT_STYLE = get_style_by_name("colorful")
|
||||||
HAVE_PYGMENTS = True
|
HAVE_PYGMENTS = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAVE_PYGMENTS = False
|
HAVE_PYGMENTS = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import sqlparse
|
import sqlparse # pyright: ignore
|
||||||
|
|
||||||
HAVE_SQLPARSE = True
|
HAVE_SQLPARSE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
HAVE_SQLPARSE = False
|
HAVE_SQLPARSE = False
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
from markupsafe import Markup
|
def format_fname(value: str) -> str:
|
||||||
|
|
||||||
|
|
||||||
def format_fname(value):
|
|
||||||
# If the value has a builtin prefix, return it unchanged
|
# If the value has a builtin prefix, return it unchanged
|
||||||
if value.startswith(('{', '<')):
|
if value.startswith(("{", "<")):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
value = os.path.normpath(value)
|
value = os.path.normpath(value)
|
||||||
@@ -35,23 +40,26 @@ def format_fname(value):
|
|||||||
# If the file is absolute, try normalizing it relative to the project root
|
# If the file is absolute, try normalizing it relative to the project root
|
||||||
# to handle it as a project file
|
# to handle it as a project file
|
||||||
if os.path.isabs(value):
|
if os.path.isabs(value):
|
||||||
value = _shortest_relative_path(
|
value = _shortest_relative_path(value, [current_app.root_path], os.path)
|
||||||
value, [current_app.root_path], os.path)
|
|
||||||
|
|
||||||
# If the value is a relative path, it is a project file
|
# If the value is a relative path, it is a project file
|
||||||
if not os.path.isabs(value):
|
if not os.path.isabs(value):
|
||||||
return os.path.join('.', value)
|
return os.path.join(".", value)
|
||||||
|
|
||||||
# Otherwise, normalize other paths relative to sys.path
|
# Otherwise, normalize other paths relative to sys.path
|
||||||
return '<%s>' % _shortest_relative_path(value, sys.path, os.path)
|
return f"<{_shortest_relative_path(value, sys.path, os.path)}>"
|
||||||
|
|
||||||
|
|
||||||
def _shortest_relative_path(value, paths, path_module):
|
def _shortest_relative_path(
|
||||||
|
value: str, paths: list[str], path_module: ModuleType
|
||||||
|
) -> str:
|
||||||
relpaths = _relative_paths(value, paths, path_module)
|
relpaths = _relative_paths(value, paths, path_module)
|
||||||
return min(itertools.chain(relpaths, [value]), key=len)
|
return min(itertools.chain(relpaths, [value]), key=len)
|
||||||
|
|
||||||
|
|
||||||
def _relative_paths(value, paths, path_module):
|
def _relative_paths(
|
||||||
|
value: str, paths: list[str], path_module: ModuleType
|
||||||
|
) -> c.Iterator[str]:
|
||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
try:
|
||||||
relval = path_module.relpath(value, path)
|
relval = path_module.relpath(value, path)
|
||||||
@@ -59,43 +67,45 @@ def _relative_paths(value, paths, path_module):
|
|||||||
# on Windows, relpath throws a ValueError for
|
# on Windows, relpath throws a ValueError for
|
||||||
# paths with different drives
|
# paths with different drives
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not relval.startswith(path_module.pardir):
|
if not relval.startswith(path_module.pardir):
|
||||||
yield relval
|
yield relval
|
||||||
|
|
||||||
|
|
||||||
def decode_text(value):
|
def decode_text(value: str | bytes) -> str:
|
||||||
"""
|
"""
|
||||||
Decode a text-like value for display.
|
Decode a text-like value for display.
|
||||||
|
|
||||||
Unicode values are returned unchanged. Byte strings will be decoded
|
Unicode values are returned unchanged. Byte strings will be decoded
|
||||||
with a text-safe replacement for unrecognized characters.
|
with a text-safe replacement for unrecognized characters.
|
||||||
"""
|
"""
|
||||||
if isinstance(value, bytes):
|
if isinstance(value, bytes):
|
||||||
return value.decode('ascii', 'replace')
|
return value.decode("ascii", "replace")
|
||||||
else:
|
|
||||||
return value
|
return value # pyright: ignore
|
||||||
|
|
||||||
|
|
||||||
def format_sql(query, args):
|
def format_sql(query: str | bytes, args: object) -> str:
|
||||||
if HAVE_SQLPARSE:
|
if HAVE_SQLPARSE:
|
||||||
query = sqlparse.format(query, reindent=True, keyword_case='upper')
|
query = sqlparse.format(query, reindent=True, keyword_case="upper")
|
||||||
|
|
||||||
if not HAVE_PYGMENTS:
|
if not HAVE_PYGMENTS:
|
||||||
return decode_text(query)
|
return decode_text(query)
|
||||||
|
|
||||||
return Markup(highlight(
|
return Markup(
|
||||||
query,
|
highlight(query, SqlLexer(), HtmlFormatter(noclasses=True, style=PYGMENT_STYLE))
|
||||||
SqlLexer(),
|
)
|
||||||
HtmlFormatter(noclasses=True, style=PYGMENT_STYLE)))
|
|
||||||
|
|
||||||
|
|
||||||
def gzip_compress(data, compresslevel=6):
|
def gzip_compress(data: bytes, compresslevel: int = 6) -> bytes:
|
||||||
buff = io.BytesIO()
|
buff = io.BytesIO()
|
||||||
with gzip.GzipFile(fileobj=buff, mode='wb', compresslevel=compresslevel) as f:
|
|
||||||
|
with gzip.GzipFile(fileobj=buff, mode="wb", compresslevel=compresslevel) as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
||||||
return buff.getvalue()
|
return buff.getvalue()
|
||||||
|
|
||||||
|
|
||||||
def gzip_decompress(data):
|
def gzip_decompress(data: bytes) -> bytes:
|
||||||
with gzip.GzipFile(fileobj=io.BytesIO(data), mode='rb') as f:
|
with gzip.GzipFile(fileobj=io.BytesIO(data), mode="rb") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
from flask import Flask, render_template
|
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
|
|
||||||
from flask_debugtoolbar import DebugToolbarExtension
|
|
||||||
|
|
||||||
app = Flask('basic_app')
|
|
||||||
app.config['DEBUG'] = True
|
|
||||||
app.config['SECRET_KEY'] = 'abc123'
|
|
||||||
app.config['SQLALCHEMY_RECORD_QUERIES'] = True
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# make sure these are printable in the config panel
|
|
||||||
app.config['BYTES_VALUE'] = b'\x00'
|
|
||||||
app.config['UNICODE_VALUE'] = u'\uffff'
|
|
||||||
|
|
||||||
toolbar = DebugToolbarExtension(app)
|
|
||||||
db = SQLAlchemy(app)
|
|
||||||
|
|
||||||
|
|
||||||
class Foo(db.Model):
|
|
||||||
__tablename__ = 'foo'
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
Foo.query.filter_by(id=1).all()
|
|
||||||
return render_template('basic_app.html')
|
|
||||||
|
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
db.create_all()
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
|
||||||
def mock_env_development(monkeypatch):
|
|
||||||
monkeypatch.setenv("FLASK_ENV", "development")
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
from flask_debugtoolbar import _printable
|
|
||||||
|
|
||||||
|
|
||||||
def load_app(name):
|
|
||||||
app = __import__(name).app
|
|
||||||
app.config['TESTING'] = True
|
|
||||||
return app.test_client()
|
|
||||||
|
|
||||||
|
|
||||||
def test_basic_app():
|
|
||||||
app = load_app('basic_app')
|
|
||||||
index = app.get('/')
|
|
||||||
assert index.status_code == 200
|
|
||||||
assert b'<div id="flDebug"' in index.data
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import ntpath
|
|
||||||
import posixpath
|
|
||||||
|
|
||||||
from markupsafe import Markup
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from flask_debugtoolbar.utils import (_relative_paths, _shortest_relative_path,
|
|
||||||
format_sql, decode_text, HAVE_PYGMENTS)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('value,paths,expected,path_module', [
|
|
||||||
# should yield relative path to the parent directory
|
|
||||||
('/foo/bar', ['/foo'], ['bar'], posixpath),
|
|
||||||
('c:\\foo\\bar', ['c:\\foo'], ['bar'], ntpath),
|
|
||||||
|
|
||||||
# should not yield result if no path is a parent directory
|
|
||||||
('/foo/bar', ['/baz'], [], posixpath),
|
|
||||||
('c:\\foo\\bar', ['c:\\baz'], [], ntpath),
|
|
||||||
|
|
||||||
# should only yield relative paths for parent directories
|
|
||||||
('/foo/bar', ['/foo', '/baz'], ['bar'], posixpath),
|
|
||||||
('c:\\foo\\bar', ['c:\\foo', 'c:\\baz'], ['bar'], ntpath),
|
|
||||||
|
|
||||||
# should yield all results when multiple parents match
|
|
||||||
('/foo/bar/baz', ['/foo', '/foo/bar'], ['bar/baz', 'baz'], posixpath),
|
|
||||||
('c:\\foo\\bar\\baz', ['c:\\foo', 'c:\\foo\\bar'],
|
|
||||||
['bar\\baz', 'baz'], ntpath),
|
|
||||||
|
|
||||||
# should ignore case differences on windows
|
|
||||||
('c:\\Foo\\bar', ['c:\\foo'], ['bar'], ntpath),
|
|
||||||
|
|
||||||
# should preserve original case
|
|
||||||
('/Foo/Bar', ['/Foo'], ['Bar'], posixpath),
|
|
||||||
('c:\\Foo\\Bar', ['c:\\foo'], ['Bar'], ntpath),
|
|
||||||
])
|
|
||||||
def test_relative_paths(value, paths, expected, path_module):
|
|
||||||
assert list(_relative_paths(value, paths, path_module)) == expected
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('value,paths,expected,path_module', [
|
|
||||||
# should yield relative path to the parent directory
|
|
||||||
('/foo/bar', ['/foo'], 'bar', posixpath),
|
|
||||||
('c:\\foo\\bar', ['c:\\foo'], 'bar', ntpath),
|
|
||||||
|
|
||||||
# should return the original value if no path is a parent directory
|
|
||||||
('/foo/bar', ['/baz'], '/foo/bar', posixpath),
|
|
||||||
('c:\\foo\\bar', ['c:\\baz'], 'c:\\foo\\bar', ntpath),
|
|
||||||
|
|
||||||
# should yield shortest result when multiple parents match
|
|
||||||
('/foo/bar/baz', ['/foo', '/foo/bar'], 'baz', posixpath),
|
|
||||||
('c:\\foo\\bar\\baz', ['c:\\foo', 'c:\\foo\\bar'], 'baz', ntpath),
|
|
||||||
])
|
|
||||||
def test_shortest_relative_path(value, paths, expected, path_module):
|
|
||||||
assert _shortest_relative_path(value, paths, path_module) == expected
|
|
||||||
|
|
||||||
|
|
||||||
def test_decode_text_unicode():
|
|
||||||
value = u'\uffff'
|
|
||||||
decoded = decode_text(value)
|
|
||||||
assert decoded == value
|
|
||||||
|
|
||||||
|
|
||||||
def test_decode_text_ascii():
|
|
||||||
value = 'abc'
|
|
||||||
assert decode_text(value.encode('ascii')) == value
|
|
||||||
|
|
||||||
|
|
||||||
def test_decode_text_non_ascii():
|
|
||||||
value = b'abc \xff xyz'
|
|
||||||
assert isinstance(value, bytes)
|
|
||||||
|
|
||||||
decoded = decode_text(value)
|
|
||||||
assert not isinstance(decoded, bytes)
|
|
||||||
|
|
||||||
assert decoded.startswith('abc')
|
|
||||||
assert decoded.endswith('xyz')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def no_pygments(monkeypatch):
|
|
||||||
monkeypatch.setattr('flask_debugtoolbar.utils.HAVE_PYGMENTS', False)
|
|
||||||
|
|
||||||
|
|
||||||
def test_format_sql_no_pygments(no_pygments):
|
|
||||||
sql = 'select 1'
|
|
||||||
assert format_sql(sql, {}) == sql
|
|
||||||
|
|
||||||
|
|
||||||
def test_format_sql_no_pygments_non_ascii(no_pygments):
|
|
||||||
sql = b"select '\xff'"
|
|
||||||
formatted = format_sql(sql, {})
|
|
||||||
assert formatted.startswith(u"select '")
|
|
||||||
|
|
||||||
|
|
||||||
def test_format_sql_no_pygments_escape_html(no_pygments):
|
|
||||||
sql = 'select x < 1'
|
|
||||||
formatted = format_sql(sql, {})
|
|
||||||
assert not isinstance(formatted, Markup)
|
|
||||||
assert Markup('%s') % formatted == 'select x < 1'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAVE_PYGMENTS,
|
|
||||||
reason='test requires the "Pygments" library')
|
|
||||||
def test_format_sql_pygments():
|
|
||||||
sql = 'select 1'
|
|
||||||
html = format_sql(sql, {})
|
|
||||||
assert isinstance(html, Markup)
|
|
||||||
assert html.startswith('<div')
|
|
||||||
assert 'select' in html
|
|
||||||
assert '1' in html
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not HAVE_PYGMENTS,
|
|
||||||
reason='test requires the "Pygments" library')
|
|
||||||
def test_format_sql_pygments_non_ascii():
|
|
||||||
sql = b"select 'abc \xff xyz'"
|
|
||||||
html = format_sql(sql, {})
|
|
||||||
assert isinstance(html, Markup)
|
|
||||||
assert html.startswith('<div')
|
|
||||||
assert 'select' in html
|
|
||||||
assert 'abc' in html
|
|
||||||
assert 'xyz' in html
|
|
||||||
38
tests/basic_app.py
Normal file
38
tests/basic_app.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask import render_template
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
|
from flask_debugtoolbar import DebugToolbarExtension
|
||||||
|
|
||||||
|
app = Flask("basic_app")
|
||||||
|
app.config["DEBUG"] = True
|
||||||
|
app.config["SECRET_KEY"] = "abc123"
|
||||||
|
app.config["SQLALCHEMY_RECORD_QUERIES"] = True
|
||||||
|
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:"
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# make sure these are printable in the config panel
|
||||||
|
app.config["BYTES_VALUE"] = b"\x00"
|
||||||
|
app.config["UNICODE_VALUE"] = "\uffff"
|
||||||
|
|
||||||
|
toolbar = DebugToolbarExtension(app)
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
|
||||||
|
class Foo(db.Model): # type: ignore[name-defined, misc]
|
||||||
|
__tablename__ = "foo"
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index() -> str:
|
||||||
|
Foo.query.filter_by(id=1).all()
|
||||||
|
return render_template("basic_app.html")
|
||||||
|
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
8
tests/conftest.py
Normal file
8
tests/conftest.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_env_development(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("FLASK_ENV", "development")
|
||||||
167
tests/test_toolbar.py
Normal file
167
tests/test_toolbar.py
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typing as t
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from flask import Flask
|
||||||
|
from flask import Response
|
||||||
|
from flask.testing import FlaskClient
|
||||||
|
|
||||||
|
from flask_debugtoolbar import DebugToolbarExtension
|
||||||
|
|
||||||
|
|
||||||
|
def load_app(name: str) -> FlaskClient:
|
||||||
|
app: Flask = __import__(name).app
|
||||||
|
app.config["TESTING"] = True
|
||||||
|
return app.test_client()
|
||||||
|
|
||||||
|
|
||||||
|
def test_basic_app() -> None:
|
||||||
|
app = load_app("basic_app")
|
||||||
|
index = app.get("/")
|
||||||
|
assert index.status_code == 200
|
||||||
|
assert b'<div id="flDebug"' in index.data
|
||||||
|
|
||||||
|
|
||||||
|
def app_with_config(
|
||||||
|
app_config: dict[str, t.Any], toolbar_config: dict[str, t.Any]
|
||||||
|
) -> Flask:
|
||||||
|
app = Flask(__name__, **app_config)
|
||||||
|
app.config["DEBUG"] = True
|
||||||
|
app.config["SECRET_KEY"] = "abc123"
|
||||||
|
|
||||||
|
for key, value in toolbar_config.items():
|
||||||
|
app.config[key] = value
|
||||||
|
|
||||||
|
DebugToolbarExtension(app)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def test_toolbar_is_host_matching_but_flask_is_not() -> None:
|
||||||
|
with pytest.raises(ValueError) as e:
|
||||||
|
app_with_config(
|
||||||
|
app_config=dict(host_matching=False),
|
||||||
|
toolbar_config=dict(
|
||||||
|
DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST="myapp.com"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert str(e.value) == (
|
||||||
|
"`DEBUG_TB_ROUTES_HOST` should only be set if your Flask app is "
|
||||||
|
"using `host_matching`."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_flask_is_host_matching_but_toolbar_is_not() -> None:
|
||||||
|
with pytest.warns(UserWarning) as record:
|
||||||
|
app_with_config(
|
||||||
|
app_config=dict(host_matching=True, static_host="static.com"),
|
||||||
|
toolbar_config=dict(DEBUG_TB_ENABLED=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(record[0].message, UserWarning)
|
||||||
|
assert record[0].message.args[0] == (
|
||||||
|
"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."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_toolbar_host_variables_rejected() -> None:
|
||||||
|
with pytest.raises(ValueError) as e:
|
||||||
|
app_with_config(
|
||||||
|
app_config=dict(host_matching=True, static_host="static.com"),
|
||||||
|
toolbar_config=dict(
|
||||||
|
DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST="<host>.com"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert str(e.value) == (
|
||||||
|
"`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."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_toolbar_in_host_mode_injects_toolbar_html() -> None:
|
||||||
|
app = app_with_config(
|
||||||
|
app_config=dict(host_matching=True, static_host="static.com"),
|
||||||
|
toolbar_config=dict(DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST="myapp.com"),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/", host="myapp.com")
|
||||||
|
def index() -> str:
|
||||||
|
return "<html><head></head><body>OK</body></html>"
|
||||||
|
|
||||||
|
with app.test_client() as client:
|
||||||
|
with app.app_context():
|
||||||
|
response = client.get("/", headers={"Host": "myapp.com"})
|
||||||
|
assert '<div id="flDebug" ' in response.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"tb_routes_host, request_host, expected_static_path",
|
||||||
|
(
|
||||||
|
("myapp.com", "myapp.com", "/_debug_toolbar/static/"),
|
||||||
|
("toolbar.com", "myapp.com", "http://toolbar.com/_debug_toolbar/static/"),
|
||||||
|
("*", "myapp.com", "/_debug_toolbar/static/"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_toolbar_injects_expected_static_path_for_host(
|
||||||
|
tb_routes_host: str, request_host: str, expected_static_path: str
|
||||||
|
) -> None:
|
||||||
|
app = app_with_config(
|
||||||
|
app_config=dict(host_matching=True, static_host="static.com"),
|
||||||
|
toolbar_config=dict(DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST=tb_routes_host),
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/", host=request_host)
|
||||||
|
def index() -> str:
|
||||||
|
return "<html><head></head><body>OK</body></html>"
|
||||||
|
|
||||||
|
with app.test_client() as client:
|
||||||
|
with app.app_context():
|
||||||
|
response = client.get("/", headers={"Host": request_host})
|
||||||
|
|
||||||
|
assert (
|
||||||
|
"""<script type="text/javascript">"""
|
||||||
|
f"""var DEBUG_TOOLBAR_STATIC_PATH = '{expected_static_path}'"""
|
||||||
|
"""</script>"""
|
||||||
|
) in response.text
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"flask.helpers.werkzeug.utils.send_from_directory",
|
||||||
|
return_value=Response(b"some-file", mimetype="text/css", status=200),
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"tb_routes_host, request_host, expected_status_code",
|
||||||
|
(
|
||||||
|
("toolbar.com", "toolbar.com", 200),
|
||||||
|
("toolbar.com", "myapp.com", 404),
|
||||||
|
("toolbar.com", "static.com", 404),
|
||||||
|
("*", "toolbar.com", 200),
|
||||||
|
("*", "myapp.com", 200),
|
||||||
|
("*", "static.com", 200),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def test_toolbar_serves_assets_based_on_host_configuration(
|
||||||
|
mock_send_from_directory: MagicMock,
|
||||||
|
tb_routes_host: str,
|
||||||
|
request_host: str,
|
||||||
|
expected_status_code: int,
|
||||||
|
) -> None:
|
||||||
|
app = app_with_config(
|
||||||
|
app_config=dict(host_matching=True, static_host="static.com"),
|
||||||
|
toolbar_config=dict(DEBUG_TB_ENABLED=True, DEBUG_TB_ROUTES_HOST=tb_routes_host),
|
||||||
|
)
|
||||||
|
|
||||||
|
with app.test_client() as client:
|
||||||
|
with app.app_context():
|
||||||
|
response = client.get(
|
||||||
|
"/_debug_toolbar/static/js/toolbar.js", headers={"Host": request_host}
|
||||||
|
)
|
||||||
|
assert response.status_code == expected_status_code
|
||||||
132
tests/test_utils.py
Normal file
132
tests/test_utils.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ntpath
|
||||||
|
import posixpath
|
||||||
|
from types import ModuleType
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from markupsafe import escape
|
||||||
|
from markupsafe import Markup
|
||||||
|
|
||||||
|
from flask_debugtoolbar.utils import _relative_paths
|
||||||
|
from flask_debugtoolbar.utils import _shortest_relative_path
|
||||||
|
from flask_debugtoolbar.utils import decode_text
|
||||||
|
from flask_debugtoolbar.utils import format_sql
|
||||||
|
from flask_debugtoolbar.utils import HAVE_PYGMENTS
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"value,paths,expected,path_module",
|
||||||
|
[
|
||||||
|
# should yield relative path to the parent directory
|
||||||
|
("/foo/bar", ["/foo"], ["bar"], posixpath),
|
||||||
|
("c:\\foo\\bar", ["c:\\foo"], ["bar"], ntpath),
|
||||||
|
# should not yield result if no path is a parent directory
|
||||||
|
("/foo/bar", ["/baz"], [], posixpath),
|
||||||
|
("c:\\foo\\bar", ["c:\\baz"], [], ntpath),
|
||||||
|
# should only yield relative paths for parent directories
|
||||||
|
("/foo/bar", ["/foo", "/baz"], ["bar"], posixpath),
|
||||||
|
("c:\\foo\\bar", ["c:\\foo", "c:\\baz"], ["bar"], ntpath),
|
||||||
|
# should yield all results when multiple parents match
|
||||||
|
("/foo/bar/baz", ["/foo", "/foo/bar"], ["bar/baz", "baz"], posixpath),
|
||||||
|
("c:\\foo\\bar\\baz", ["c:\\foo", "c:\\foo\\bar"], ["bar\\baz", "baz"], ntpath),
|
||||||
|
# should ignore case differences on windows
|
||||||
|
("c:\\Foo\\bar", ["c:\\foo"], ["bar"], ntpath),
|
||||||
|
# should preserve original case
|
||||||
|
("/Foo/Bar", ["/Foo"], ["Bar"], posixpath),
|
||||||
|
("c:\\Foo\\Bar", ["c:\\foo"], ["Bar"], ntpath),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_relative_paths(
|
||||||
|
value: str, paths: list[str], expected: list[str], path_module: ModuleType
|
||||||
|
) -> None:
|
||||||
|
assert list(_relative_paths(value, paths, path_module)) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"value,paths,expected,path_module",
|
||||||
|
[
|
||||||
|
# should yield relative path to the parent directory
|
||||||
|
("/foo/bar", ["/foo"], "bar", posixpath),
|
||||||
|
("c:\\foo\\bar", ["c:\\foo"], "bar", ntpath),
|
||||||
|
# should return the original value if no path is a parent directory
|
||||||
|
("/foo/bar", ["/baz"], "/foo/bar", posixpath),
|
||||||
|
("c:\\foo\\bar", ["c:\\baz"], "c:\\foo\\bar", ntpath),
|
||||||
|
# should yield shortest result when multiple parents match
|
||||||
|
("/foo/bar/baz", ["/foo", "/foo/bar"], "baz", posixpath),
|
||||||
|
("c:\\foo\\bar\\baz", ["c:\\foo", "c:\\foo\\bar"], "baz", ntpath),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_shortest_relative_path(
|
||||||
|
value: str, paths: list[str], expected: str, path_module: ModuleType
|
||||||
|
) -> None:
|
||||||
|
assert _shortest_relative_path(value, paths, path_module) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_text_unicode() -> None:
|
||||||
|
value = "\uffff"
|
||||||
|
decoded = decode_text(value)
|
||||||
|
assert decoded == value
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_text_ascii() -> None:
|
||||||
|
value = "abc"
|
||||||
|
assert decode_text(value.encode("ascii")) == value
|
||||||
|
|
||||||
|
|
||||||
|
def test_decode_text_non_ascii() -> None:
|
||||||
|
value = b"abc \xff xyz"
|
||||||
|
assert isinstance(value, bytes)
|
||||||
|
|
||||||
|
decoded = decode_text(value)
|
||||||
|
assert not isinstance(decoded, bytes)
|
||||||
|
|
||||||
|
assert decoded.startswith("abc")
|
||||||
|
assert decoded.endswith("xyz")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def no_pygments(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr("flask_debugtoolbar.utils.HAVE_PYGMENTS", False)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("no_pygments")
|
||||||
|
def test_format_sql_no_pygments() -> None:
|
||||||
|
sql = "select 1"
|
||||||
|
assert format_sql(sql, {}) == sql
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("no_pygments")
|
||||||
|
def test_format_sql_no_pygments_non_ascii() -> None:
|
||||||
|
sql = b"select '\xff'"
|
||||||
|
formatted = format_sql(sql, {})
|
||||||
|
assert formatted.startswith("select '")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("no_pygments")
|
||||||
|
def test_format_sql_no_pygments_escape_html() -> None:
|
||||||
|
sql = "select x < 1"
|
||||||
|
formatted = format_sql(sql, {})
|
||||||
|
assert not isinstance(formatted, Markup)
|
||||||
|
assert escape(formatted) == "select x < 1"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HAVE_PYGMENTS, reason='test requires the "Pygments" library')
|
||||||
|
def test_format_sql_pygments() -> None:
|
||||||
|
sql = "select 1"
|
||||||
|
html = format_sql(sql, {})
|
||||||
|
assert isinstance(html, Markup)
|
||||||
|
assert html.startswith("<div")
|
||||||
|
assert "select" in html
|
||||||
|
assert "1" in html
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HAVE_PYGMENTS, reason='test requires the "Pygments" library')
|
||||||
|
def test_format_sql_pygments_non_ascii() -> None:
|
||||||
|
sql = b"select 'abc \xff xyz'"
|
||||||
|
html = format_sql(sql, {})
|
||||||
|
assert isinstance(html, Markup)
|
||||||
|
assert html.startswith("<div")
|
||||||
|
assert "select" in html
|
||||||
|
assert "abc" in html
|
||||||
|
assert "xyz" in html
|
||||||
66
tox.ini
66
tox.ini
@@ -1,31 +1,55 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist =
|
envlist =
|
||||||
py3{12,11,10,9,8,7}
|
py3{12,11,10,9,8}
|
||||||
stylecheck
|
|
||||||
minimal
|
minimal
|
||||||
skip_missing_interpreters = True
|
style
|
||||||
|
typing
|
||||||
|
docs
|
||||||
|
skip_missing_interpreters = true
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
deps =
|
package = wheel
|
||||||
pytest
|
wheel_build_env = .pkg
|
||||||
Flask-SQLAlchemy
|
constrain_package_deps = true
|
||||||
Pygments
|
use_frozen_constraints = true
|
||||||
commands =
|
deps = -r requirements/tests.txt
|
||||||
pytest
|
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
|
||||||
|
|
||||||
[testenv:minimal]
|
[testenv:minimal]
|
||||||
deps =
|
deps =
|
||||||
.
|
commands = python -c "from flask_debugtoolbar import DebugToolbarExtension"
|
||||||
commands =
|
|
||||||
python -c "from flask_debugtoolbar import DebugToolbarExtension"
|
|
||||||
|
|
||||||
[testenv:stylecheck]
|
[testenv:style]
|
||||||
deps =
|
deps = pre-commit
|
||||||
pycodestyle
|
skip_install = true
|
||||||
commands =
|
commands = pre-commit run --all-files
|
||||||
# E731: do not assign a lambda expression, use a def
|
|
||||||
# W504: line break after binary operator
|
|
||||||
pycodestyle src/flask_debugtoolbar test --ignore=E731,W504
|
|
||||||
|
|
||||||
[pycodestyle]
|
[testenv:typing]
|
||||||
max-line-length = 100
|
deps = -r requirements/typing.txt
|
||||||
|
commands =
|
||||||
|
mypy
|
||||||
|
pyright
|
||||||
|
pyright --verifytypes flask_debugtoolbar --ignoreexternal
|
||||||
|
|
||||||
|
[testenv:docs]
|
||||||
|
deps = -r requirements/docs.txt
|
||||||
|
commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml
|
||||||
|
|
||||||
|
[testenv:update-pre_commit]
|
||||||
|
labels = update
|
||||||
|
deps = pre-commit
|
||||||
|
skip_install = true
|
||||||
|
commands = pre-commit autoupdate -j4
|
||||||
|
|
||||||
|
[testenv:update-requirements]
|
||||||
|
base_python = 3.8
|
||||||
|
labels = update
|
||||||
|
deps = pip-tools
|
||||||
|
skip_install = true
|
||||||
|
change_dir = requirements
|
||||||
|
commands =
|
||||||
|
pip-compile build.in -q {posargs:-U}
|
||||||
|
pip-compile docs.in -q {posargs:-U}
|
||||||
|
pip-compile tests.in -q {posargs:-U}
|
||||||
|
pip-compile typing.in -q {posargs:-U}
|
||||||
|
pip-compile dev.in -q {posargs:-U}
|
||||||
|
|||||||
Reference in New Issue
Block a user