mirror of
https://github.com/pallets-eco/flask-debugtoolbar.git
synced 2025-12-30 18:19:31 -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__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
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
|
||||
.coverage*
|
||||
htmlcov/
|
||||
.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/
|
||||
|
||||
# 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
|
||||
|
||||
# Set the OS, Python version and other tools you might need
|
||||
build:
|
||||
os: ubuntu-lts-latest
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "latest"
|
||||
# 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: '3.12'
|
||||
python:
|
||||
install:
|
||||
- path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
- requirements: requirements/docs.txt
|
||||
- method: pip
|
||||
path: .
|
||||
sphinx:
|
||||
builder: dirhtml
|
||||
fail_on_warning: true
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
Copyright (c) Rob Hudson and individual contributors.
|
||||
All rights reserved.
|
||||
Copyright 2011 Pallets Community Ecosystem
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of Django nor the names of its contributors may be used
|
||||
to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
@@ -1,3 +0,0 @@
|
||||
include LICENSE
|
||||
recursive-include 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 -*-
|
||||
#
|
||||
# Flask-DebugToolbar documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed Feb 15 18:08:39 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
import importlib.metadata
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import pkg_resources
|
||||
import sys
|
||||
import time
|
||||
# Project --------------------------------------------------------------
|
||||
|
||||
import flask_debugtoolbar
|
||||
project = "Flask-DebugToolbar"
|
||||
version = release = importlib.metadata.version("flask-debugtoolbar").partition(".dev")[
|
||||
0
|
||||
]
|
||||
|
||||
# General --------------------------------------------------------------
|
||||
|
||||
BUILD_DATE = datetime.datetime.utcfromtimestamp(int(os.environ.get('SOURCE_DATE_EPOCH', time.time())))
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
default_role = "code"
|
||||
extensions = [
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx.ext.intersphinx',
|
||||
'pallets_sphinx_themes',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.extlinks",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinxcontrib.log_cabinet",
|
||||
"pallets_sphinx_themes",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Flask-DebugToolbar'
|
||||
copyright = u'2012-{0}'.format(BUILD_DATE.year)
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = flask_debugtoolbar.__version__
|
||||
# The short X.Y version.
|
||||
version = '.'.join(release.split('.')[:2])
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
autodoc_member_order = "bysource"
|
||||
autodoc_typehints = "description"
|
||||
autodoc_preserve_defaults = True
|
||||
extlinks = {
|
||||
"issue": ("https://github.com/pallets-eco/flask-debugtoolbar/issues/%s", "#%s"),
|
||||
"pr": ("https://github.com/pallets-eco/flask-debugtoolbar/pull/%s", "#%s"),
|
||||
}
|
||||
intersphinx_mapping = {
|
||||
'flasksqlalchemy': ('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 ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'flask'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
sys.path.append(os.path.abspath('_themes'))
|
||||
html_theme_path = ['_themes']
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'Flask-DebugToolbardoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'Flask-DebugToolbar.tex', u'Flask-DebugToolbar Documentation',
|
||||
u'Matt Good', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'flask-debugtoolbar', u'Flask-DebugToolbar Documentation',
|
||||
[u'Matt Good'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output ------------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'Flask-DebugToolbar', u'Flask-DebugToolbar Documentation',
|
||||
u'Matt Good', 'Flask-DebugToolbar', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
html_theme = "flask"
|
||||
html_static_path = ["_static"]
|
||||
html_copy_source = False
|
||||
html_show_copyright = False
|
||||
html_use_index = False
|
||||
html_domain_indices = False
|
||||
|
||||
@@ -55,6 +55,13 @@ Name Description De
|
||||
==================================== ===================================== ==========================
|
||||
``DEBUG_TB_ENABLED`` Enable the toolbar? ``app.debug``
|
||||
``DEBUG_TB_HOSTS`` Whitelist of hosts to display toolbar any host
|
||||
``DEBUG_TB_ROUTES_HOST`` The host to associate with toolbar ``None``
|
||||
routes (where its assets are served
|
||||
from), or the sentinel value `*` to
|
||||
serve from the same host as the
|
||||
current request (ie any host). This
|
||||
is only required if Flask is
|
||||
configured to use `host_matching`.
|
||||
``DEBUG_TB_INTERCEPT_REDIRECTS`` Should intercept redirects? ``True``
|
||||
``DEBUG_TB_PANELS`` List of module/class names of panels enable all built-in panels
|
||||
``DEBUG_TB_PROFILER_ENABLED`` Enable the profiler on all requests ``False``, user-enabled
|
||||
@@ -73,7 +80,8 @@ Panels
|
||||
|
||||
.. toctree::
|
||||
|
||||
panels
|
||||
panels
|
||||
license
|
||||
|
||||
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
|
||||
.. _Michael van Tellingen: https://github.com/mvantellingen
|
||||
.. _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`
|
||||
|
||||
from flask import Flask, render_template, redirect, url_for
|
||||
from flask import Flask
|
||||
from flask import redirect
|
||||
from flask import render_template
|
||||
from flask import url_for
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from flask_debugtoolbar import DebugToolbarExtension
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = True
|
||||
#app.config['DEBUG_TB_PANELS'] = (
|
||||
app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = True
|
||||
# app.config['DEBUG_TB_PANELS'] = (
|
||||
# 'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
|
||||
# 'flask_debugtoolbar.panels.logger.LoggingPanel',
|
||||
# 'flask_debugtoolbar.panels.timer.TimerDebugPanel',
|
||||
#)
|
||||
#app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' )
|
||||
app.config['SECRET_KEY'] = 'asd'
|
||||
app.config['SQLALCHEMY_RECORD_QUERIES'] = True
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
|
||||
# This is no longer needed for Flask-SQLAlchemy 3.0+, if you're using 2.X you'll want to define this:
|
||||
# )
|
||||
# app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' )
|
||||
app.config["SECRET_KEY"] = "asd"
|
||||
app.config["SQLALCHEMY_RECORD_QUERIES"] = True
|
||||
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:////tmp/test.db"
|
||||
# This is no longer needed for Flask-SQLAlchemy 3.0+, if you're using 2.X you'll
|
||||
# want to define this:
|
||||
# app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
@@ -24,21 +28,21 @@ toolbar = DebugToolbarExtension(app)
|
||||
|
||||
|
||||
class ExampleModel(db.Model):
|
||||
__tablename__ = 'examples'
|
||||
__tablename__ = "examples"
|
||||
value = db.Column(db.String(100), primary_key=True)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
def index():
|
||||
app.logger.info("Hello there")
|
||||
ExampleModel.query.get(1)
|
||||
return render_template('index.html')
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route('/redirect')
|
||||
@app.route("/redirect")
|
||||
def redirect_example():
|
||||
response = redirect(url_for('index'))
|
||||
response.set_cookie('test_cookie', '1')
|
||||
response = redirect(url_for("index"))
|
||||
response.set_cookie("test_cookie", "1")
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,84 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel"
|
||||
[project]
|
||||
name = "Flask-DebugToolbar"
|
||||
version = "0.16.0"
|
||||
description = "A toolbar overlay for debugging Flask applications."
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE.txt" }
|
||||
author = [{ name = "Michael van Tellingen" }]
|
||||
maintainers = [{ name = "Pallets Ecosystem", email = "contact@palletsprojects.com" }]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Framework :: Flask",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Programming Language :: Python",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"flask>=2.3.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://flask-debugtoolbar.readthedocs.io"
|
||||
Changes = "https://github.com/pallets-eco/flask-debugtoolbar/releases/"
|
||||
Source = "https://github.com/pallets-eco/flask-debugtoolbar/"
|
||||
Chat = "https://discord.gg/pallets"
|
||||
|
||||
[build-system]
|
||||
requires = ["flit_core<4"]
|
||||
build-backend = "flit_core.buildapi"
|
||||
|
||||
[tool.flit.module]
|
||||
name = "flask_debugtoolbar"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
filterwarnings = [
|
||||
"error",
|
||||
]
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
source = ["flask_debugtoolbar", "tests"]
|
||||
|
||||
[tool.coverage.paths]
|
||||
source = ["src", "*/site-packages"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
files = ["src/flask_debugtoolbar", "tests"]
|
||||
show_error_codes = true
|
||||
pretty = true
|
||||
strict = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = [
|
||||
"sqlparse.*"
|
||||
]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pyright]
|
||||
pythonVersion = "3.8"
|
||||
include = ["src/flask_debugtoolbar", "tests"]
|
||||
typeCheckingMode = "basic"
|
||||
|
||||
[tool.ruff]
|
||||
src = ["src"]
|
||||
fix = true
|
||||
show-fixes = true
|
||||
output-format = "full"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"B", # flake8-bugbear
|
||||
"E", # pycodestyle error
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle warning
|
||||
]
|
||||
ignore-init-module-imports = true
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-single-line = true
|
||||
order-by-type = false
|
||||
|
||||
1
requirements/build.in
Normal file
1
requirements/build.in
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
18
requirements/build.txt
Normal file
18
requirements/build.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile build.in
|
||||
#
|
||||
build==1.2.2
|
||||
# via -r build.in
|
||||
importlib-metadata==7.1.0
|
||||
# via build
|
||||
packaging==24.0
|
||||
# via build
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
tomli==2.0.1
|
||||
# via build
|
||||
zipp==3.19.1
|
||||
# via importlib-metadata
|
||||
5
requirements/dev.in
Normal file
5
requirements/dev.in
Normal file
@@ -0,0 +1,5 @@
|
||||
-r docs.txt
|
||||
-r tests.txt
|
||||
-r typing.txt
|
||||
pre-commit
|
||||
tox
|
||||
257
requirements/dev.txt
Normal file
257
requirements/dev.txt
Normal file
@@ -0,0 +1,257 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile dev.in
|
||||
#
|
||||
alabaster==0.7.13
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
babel==2.14.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
blinker==1.8.1
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
cachetools==5.3.3
|
||||
# via tox
|
||||
certifi==2024.7.4
|
||||
# via
|
||||
# -r docs.txt
|
||||
# requests
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
chardet==5.2.0
|
||||
# via tox
|
||||
charset-normalizer==3.3.2
|
||||
# via
|
||||
# -r docs.txt
|
||||
# requests
|
||||
click==8.1.7
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
colorama==0.4.6
|
||||
# via tox
|
||||
distlib==0.3.8
|
||||
# via virtualenv
|
||||
docutils==0.20.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
exceptiongroup==1.2.1
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# pytest
|
||||
filelock==3.14.0
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
flask==3.0.3
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask-sqlalchemy
|
||||
flask-sqlalchemy==3.1.1
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
greenlet==3.0.3
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# sqlalchemy
|
||||
identify==2.5.36
|
||||
# via pre-commit
|
||||
idna==3.7
|
||||
# via
|
||||
# -r docs.txt
|
||||
# requests
|
||||
imagesize==1.4.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
importlib-metadata==7.1.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
# sphinx
|
||||
iniconfig==2.0.0
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# pytest
|
||||
itsdangerous==2.2.0
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
jinja2==3.1.4
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
# sphinx
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# jinja2
|
||||
# werkzeug
|
||||
mypy==1.11.2
|
||||
# via -r typing.txt
|
||||
mypy-extensions==1.0.0
|
||||
# via
|
||||
# -r typing.txt
|
||||
# mypy
|
||||
nodeenv==1.8.0
|
||||
# via
|
||||
# -r typing.txt
|
||||
# pre-commit
|
||||
# pyright
|
||||
packaging==24.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# pallets-sphinx-themes
|
||||
# pyproject-api
|
||||
# pytest
|
||||
# sphinx
|
||||
# tox
|
||||
pallets-sphinx-themes==2.1.3
|
||||
# via -r docs.txt
|
||||
platformdirs==4.2.1
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
pluggy==1.5.0
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# pytest
|
||||
# tox
|
||||
pre-commit==3.5.0
|
||||
# via -r dev.in
|
||||
pygments==2.18.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# sphinx
|
||||
pyproject-api==1.6.1
|
||||
# via tox
|
||||
pyright==1.1.382.post1
|
||||
# via -r typing.txt
|
||||
pytest==8.3.3
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
pytz==2024.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# babel
|
||||
pyyaml==6.0.1
|
||||
# via pre-commit
|
||||
requests==2.32.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
snowballstemmer==2.2.0
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinx==7.1.2
|
||||
# via
|
||||
# -r docs.txt
|
||||
# pallets-sphinx-themes
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinxcontrib-applehelp==1.0.4
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r docs.txt
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
# via
|
||||
# -r docs.txt
|
||||
# sphinx
|
||||
sqlalchemy==2.0.29
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask-sqlalchemy
|
||||
tomli==2.0.1
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# mypy
|
||||
# pyproject-api
|
||||
# pytest
|
||||
# tox
|
||||
tox==4.15.1
|
||||
# via -r dev.in
|
||||
types-docutils==0.21.0.20240423
|
||||
# via
|
||||
# -r typing.txt
|
||||
# types-pygments
|
||||
types-pygments==2.18.0.20240506
|
||||
# via -r typing.txt
|
||||
types-setuptools==69.5.0.20240423
|
||||
# via
|
||||
# -r typing.txt
|
||||
# types-pygments
|
||||
typing-extensions==4.11.0
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# mypy
|
||||
# pyright
|
||||
# sqlalchemy
|
||||
urllib3==2.2.2
|
||||
# via
|
||||
# -r docs.txt
|
||||
# requests
|
||||
virtualenv==20.26.1
|
||||
# via
|
||||
# pre-commit
|
||||
# tox
|
||||
werkzeug==3.0.3
|
||||
# via
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# flask
|
||||
zipp==3.19.1
|
||||
# via
|
||||
# -r docs.txt
|
||||
# -r tests.txt
|
||||
# -r typing.txt
|
||||
# importlib-metadata
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
3
requirements/docs.in
Normal file
3
requirements/docs.in
Normal file
@@ -0,0 +1,3 @@
|
||||
pallets-sphinx-themes
|
||||
sphinx
|
||||
sphinxcontrib-log-cabinet
|
||||
63
requirements/docs.txt
Normal file
63
requirements/docs.txt
Normal file
@@ -0,0 +1,63 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile docs.in
|
||||
#
|
||||
alabaster==0.7.13
|
||||
# via sphinx
|
||||
babel==2.14.0
|
||||
# via sphinx
|
||||
certifi==2024.7.4
|
||||
# via requests
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
docutils==0.20.1
|
||||
# via sphinx
|
||||
idna==3.7
|
||||
# via requests
|
||||
imagesize==1.4.1
|
||||
# via sphinx
|
||||
importlib-metadata==7.1.0
|
||||
# via sphinx
|
||||
jinja2==3.1.4
|
||||
# via sphinx
|
||||
markupsafe==2.1.5
|
||||
# via jinja2
|
||||
packaging==24.0
|
||||
# via
|
||||
# pallets-sphinx-themes
|
||||
# sphinx
|
||||
pallets-sphinx-themes==2.1.3
|
||||
# via -r docs.in
|
||||
pygments==2.18.0
|
||||
# via sphinx
|
||||
pytz==2024.1
|
||||
# via babel
|
||||
requests==2.32.0
|
||||
# via sphinx
|
||||
snowballstemmer==2.2.0
|
||||
# via sphinx
|
||||
sphinx==7.1.2
|
||||
# via
|
||||
# -r docs.in
|
||||
# pallets-sphinx-themes
|
||||
# sphinxcontrib-log-cabinet
|
||||
sphinxcontrib-applehelp==1.0.4
|
||||
# via sphinx
|
||||
sphinxcontrib-devhelp==1.0.2
|
||||
# via sphinx
|
||||
sphinxcontrib-htmlhelp==2.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-jsmath==1.0.1
|
||||
# via sphinx
|
||||
sphinxcontrib-log-cabinet==1.0.1
|
||||
# via -r docs.in
|
||||
sphinxcontrib-qthelp==1.0.3
|
||||
# via sphinx
|
||||
sphinxcontrib-serializinghtml==1.1.5
|
||||
# via sphinx
|
||||
urllib3==2.2.2
|
||||
# via requests
|
||||
zipp==3.19.1
|
||||
# via importlib-metadata
|
||||
3
requirements/tests.in
Normal file
3
requirements/tests.in
Normal file
@@ -0,0 +1,3 @@
|
||||
pytest
|
||||
flask-sqlalchemy
|
||||
pygments
|
||||
48
requirements/tests.txt
Normal file
48
requirements/tests.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile tests.in
|
||||
#
|
||||
blinker==1.8.1
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
exceptiongroup==1.2.1
|
||||
# via pytest
|
||||
flask==3.0.3
|
||||
# via flask-sqlalchemy
|
||||
flask-sqlalchemy==3.1.1
|
||||
# via -r tests.in
|
||||
greenlet==3.0.3
|
||||
# via sqlalchemy
|
||||
importlib-metadata==7.1.0
|
||||
# via flask
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via flask
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
packaging==24.0
|
||||
# via pytest
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
pygments==2.18.0
|
||||
# via -r tests.in
|
||||
pytest==8.3.3
|
||||
# via -r tests.in
|
||||
sqlalchemy==2.0.29
|
||||
# via flask-sqlalchemy
|
||||
tomli==2.0.1
|
||||
# via pytest
|
||||
typing-extensions==4.11.0
|
||||
# via sqlalchemy
|
||||
werkzeug==3.0.3
|
||||
# via flask
|
||||
zipp==3.19.1
|
||||
# via importlib-metadata
|
||||
5
requirements/typing.in
Normal file
5
requirements/typing.in
Normal file
@@ -0,0 +1,5 @@
|
||||
mypy
|
||||
pyright
|
||||
pytest
|
||||
types-pygments
|
||||
flask-sqlalchemy
|
||||
68
requirements/typing.txt
Normal file
68
requirements/typing.txt
Normal file
@@ -0,0 +1,68 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.8
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile typing.in
|
||||
#
|
||||
blinker==1.8.1
|
||||
# via flask
|
||||
click==8.1.7
|
||||
# via flask
|
||||
exceptiongroup==1.2.1
|
||||
# via pytest
|
||||
flask==3.0.3
|
||||
# via flask-sqlalchemy
|
||||
flask-sqlalchemy==3.1.1
|
||||
# via -r typing.in
|
||||
greenlet==3.0.3
|
||||
# via sqlalchemy
|
||||
importlib-metadata==7.1.0
|
||||
# via flask
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
itsdangerous==2.2.0
|
||||
# via flask
|
||||
jinja2==3.1.4
|
||||
# via flask
|
||||
markupsafe==2.1.5
|
||||
# via
|
||||
# jinja2
|
||||
# werkzeug
|
||||
mypy==1.11.2
|
||||
# via -r typing.in
|
||||
mypy-extensions==1.0.0
|
||||
# via mypy
|
||||
nodeenv==1.8.0
|
||||
# via pyright
|
||||
packaging==24.0
|
||||
# via pytest
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
pyright==1.1.382.post1
|
||||
# via -r typing.in
|
||||
pytest==8.3.3
|
||||
# via -r typing.in
|
||||
sqlalchemy==2.0.29
|
||||
# via flask-sqlalchemy
|
||||
tomli==2.0.1
|
||||
# via
|
||||
# mypy
|
||||
# pytest
|
||||
types-docutils==0.21.0.20240423
|
||||
# via types-pygments
|
||||
types-pygments==2.18.0.20240506
|
||||
# via -r typing.in
|
||||
types-setuptools==69.5.0.20240423
|
||||
# via types-pygments
|
||||
typing-extensions==4.11.0
|
||||
# via
|
||||
# mypy
|
||||
# pyright
|
||||
# sqlalchemy
|
||||
werkzeug==3.0.3
|
||||
# via flask
|
||||
zipp==3.19.1
|
||||
# via importlib-metadata
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
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 typing as t
|
||||
import urllib.parse
|
||||
import warnings
|
||||
from contextvars import ContextVar
|
||||
|
||||
import flask
|
||||
from flask import Blueprint, current_app, request, g, send_from_directory, url_for
|
||||
import jinja2.ext
|
||||
from flask import Blueprint
|
||||
from flask import current_app
|
||||
from flask import Flask
|
||||
from flask import g
|
||||
from flask import request
|
||||
from flask import send_from_directory
|
||||
from flask import url_for
|
||||
from flask.globals import request_ctx
|
||||
from jinja2 import Environment
|
||||
from jinja2 import PackageLoader
|
||||
from werkzeug import Request
|
||||
from werkzeug import Response
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
from jinja2 import __version__ as __jinja_version__
|
||||
from jinja2 import Environment, PackageLoader
|
||||
from .toolbar import DebugToolbar
|
||||
from .utils import decode_text
|
||||
from .utils import gzip_compress
|
||||
from .utils import gzip_decompress
|
||||
|
||||
from flask_debugtoolbar.compat import iteritems
|
||||
from flask_debugtoolbar.toolbar import DebugToolbar
|
||||
from flask_debugtoolbar.utils import decode_text, gzip_compress, gzip_decompress
|
||||
|
||||
try:
|
||||
# Python 3.8+
|
||||
from importlib.metadata import version
|
||||
|
||||
__version__ = version("Flask-DebugToolbar")
|
||||
except ImportError:
|
||||
import pkg_resources
|
||||
|
||||
__version__ = pkg_resources.get_distribution("Flask-DebugToolbar").version
|
||||
module: Blueprint = Blueprint("debugtoolbar", __name__)
|
||||
|
||||
|
||||
module = Blueprint('debugtoolbar', __name__)
|
||||
|
||||
|
||||
def replace_insensitive(string, target, replacement):
|
||||
def replace_insensitive(string: str, target: str, replacement: str) -> str:
|
||||
"""Similar to string.replace() but is case insensitive
|
||||
Code borrowed from:
|
||||
http://forums.devshed.com/python-programming-11/case-insensitive-string-replace-490921.html
|
||||
"""
|
||||
no_case = string.lower()
|
||||
index = no_case.rfind(target.lower())
|
||||
|
||||
if index >= 0:
|
||||
return string[:index] + replacement + string[index + len(target):]
|
||||
return string[:index] + replacement + string[index + len(target) :]
|
||||
else: # no results so return the original string
|
||||
return string
|
||||
|
||||
|
||||
def _printable(value):
|
||||
def _printable(value: object) -> str:
|
||||
try:
|
||||
return decode_text(repr(value))
|
||||
except Exception as e:
|
||||
return '<repr(%s) raised %s: %s>' % (
|
||||
object.__repr__(value), type(e).__name__, e)
|
||||
return f"<repr({object.__repr__(value)}) raised {type(e).__name__}: {e}>"
|
||||
|
||||
|
||||
class DebugToolbarExtension(object):
|
||||
_static_dir = os.path.realpath(
|
||||
os.path.join(os.path.dirname(__file__), 'static'))
|
||||
class DebugToolbarExtension:
|
||||
_static_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "static"))
|
||||
|
||||
_toolbar_codes = [200, 201, 400, 401, 403, 404, 405, 500, 501, 502, 503, 504]
|
||||
_redirect_codes = [301, 302, 303, 304]
|
||||
|
||||
def __init__(self, app=None):
|
||||
def __init__(self, app: Flask | None = None) -> None:
|
||||
self.app = app
|
||||
self.toolbar_routes_host: str | None = None
|
||||
|
||||
# Support threads running `flask.copy_current_request_context` without
|
||||
# poping toolbar during `teardown_request`
|
||||
self.debug_toolbars_var = contextvars.ContextVar('debug_toolbars')
|
||||
jinja_extensions = ['jinja2.ext.i18n']
|
||||
self.debug_toolbars_var: ContextVar[dict[Request, DebugToolbar]] = ContextVar(
|
||||
"debug_toolbars"
|
||||
)
|
||||
jinja_extensions = [jinja2.ext.i18n]
|
||||
|
||||
if __jinja_version__[0] == '2':
|
||||
jinja_extensions.append('jinja2.ext.with_')
|
||||
# Jinja2<3
|
||||
if hasattr(jinja2.ext, "with_"):
|
||||
jinja_extensions.append(jinja2.ext.with_) # pyright: ignore
|
||||
|
||||
# Configure jinja for the internal templates and add url rules
|
||||
# for static data
|
||||
self.jinja_env = Environment(
|
||||
self.jinja_env: Environment = Environment(
|
||||
autoescape=True,
|
||||
extensions=jinja_extensions,
|
||||
loader=PackageLoader(__name__, 'templates'))
|
||||
self.jinja_env.filters['urlencode'] = urllib.parse.quote_plus
|
||||
self.jinja_env.filters['printable'] = _printable
|
||||
self.jinja_env.globals['url_for'] = url_for
|
||||
loader=PackageLoader(__name__, "templates"),
|
||||
)
|
||||
self.jinja_env.filters["urlencode"] = urllib.parse.quote_plus
|
||||
self.jinja_env.filters["printable"] = _printable
|
||||
self.jinja_env.globals["url_for"] = url_for
|
||||
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
for k, v in iteritems(self._default_config(app)):
|
||||
def init_app(self, app: Flask) -> None:
|
||||
for k, v in self._default_config(app).items():
|
||||
app.config.setdefault(k, v)
|
||||
|
||||
if not app.config['DEBUG_TB_ENABLED']:
|
||||
if not app.config["DEBUG_TB_ENABLED"]:
|
||||
return
|
||||
|
||||
if not app.config.get('SECRET_KEY'):
|
||||
if not app.config.get("SECRET_KEY"):
|
||||
raise RuntimeError(
|
||||
"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)
|
||||
|
||||
@@ -98,90 +108,161 @@ class DebugToolbarExtension(object):
|
||||
app.teardown_request(self.teardown_request)
|
||||
|
||||
# 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>',
|
||||
'_debug_toolbar.static', self.send_static_file)
|
||||
app.add_url_rule(
|
||||
"/_debug_toolbar/static/<path:filename>",
|
||||
"_debug_toolbar.static",
|
||||
self.send_static_file,
|
||||
host=self.toolbar_routes_host,
|
||||
)
|
||||
|
||||
app.register_blueprint(module, url_prefix='/_debug_toolbar/views')
|
||||
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 {
|
||||
'DEBUG_TB_ENABLED': app.debug,
|
||||
'DEBUG_TB_HOSTS': (),
|
||||
'DEBUG_TB_INTERCEPT_REDIRECTS': True,
|
||||
'DEBUG_TB_PANELS': (
|
||||
'flask_debugtoolbar.panels.versions.VersionDebugPanel',
|
||||
'flask_debugtoolbar.panels.timer.TimerDebugPanel',
|
||||
'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
|
||||
'flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel',
|
||||
'flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel',
|
||||
'flask_debugtoolbar.panels.template.TemplateDebugPanel',
|
||||
'flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel',
|
||||
'flask_debugtoolbar.panels.logger.LoggingPanel',
|
||||
'flask_debugtoolbar.panels.route_list.RouteListDebugPanel',
|
||||
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel',
|
||||
'flask_debugtoolbar.panels.g.GDebugPanel',
|
||||
"DEBUG_TB_ENABLED": app.debug,
|
||||
"DEBUG_TB_HOSTS": (),
|
||||
"DEBUG_TB_ROUTES_HOST": None,
|
||||
"DEBUG_TB_INTERCEPT_REDIRECTS": True,
|
||||
"DEBUG_TB_PANELS": (
|
||||
"flask_debugtoolbar.panels.versions.VersionDebugPanel",
|
||||
"flask_debugtoolbar.panels.timer.TimerDebugPanel",
|
||||
"flask_debugtoolbar.panels.headers.HeaderDebugPanel",
|
||||
"flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel",
|
||||
"flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel",
|
||||
"flask_debugtoolbar.panels.template.TemplateDebugPanel",
|
||||
"flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel",
|
||||
"flask_debugtoolbar.panels.logger.LoggingPanel",
|
||||
"flask_debugtoolbar.panels.route_list.RouteListDebugPanel",
|
||||
"flask_debugtoolbar.panels.profiler.ProfilerDebugPanel",
|
||||
"flask_debugtoolbar.panels.g.GDebugPanel",
|
||||
),
|
||||
'SQLALCHEMY_RECORD_QUERIES': app.debug,
|
||||
"SQLALCHEMY_RECORD_QUERIES": app.debug,
|
||||
}
|
||||
|
||||
def dispatch_request(self):
|
||||
"""Modified version of Flask.dispatch_request to call process_view."""
|
||||
def _validate_and_configure_toolbar_routes_host(self, app: Flask) -> None:
|
||||
toolbar_routes_host = app.config["DEBUG_TB_ROUTES_HOST"]
|
||||
if app.url_map.host_matching and not toolbar_routes_host:
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
"Flask-DebugToolbar requires DEBUG_TB_ROUTES_HOST to be set if Flask "
|
||||
"is running in `host_matching` mode. Static assets for the toolbar "
|
||||
"will not be served correctly unless this is set.",
|
||||
stacklevel=1,
|
||||
)
|
||||
|
||||
if toolbar_routes_host:
|
||||
if not app.url_map.host_matching:
|
||||
raise ValueError(
|
||||
"`DEBUG_TB_ROUTES_HOST` should only be set if your Flask app is "
|
||||
"using `host_matching`."
|
||||
)
|
||||
|
||||
if toolbar_routes_host.strip() == "*":
|
||||
toolbar_routes_host = "<toolbar_routes_host>"
|
||||
elif "<" in toolbar_routes_host and ">" in toolbar_routes_host:
|
||||
raise ValueError(
|
||||
"`DEBUG_TB_ROUTES_HOST` must either be a host name with no "
|
||||
"variables, to serve all Flask-DebugToolbar assets from a single "
|
||||
"host, or `*` to match the current request's host."
|
||||
)
|
||||
|
||||
# Automatically inject `toolbar_routes_host` into `url_for` calls for
|
||||
# the toolbar's `send_static_file` method.
|
||||
@app.url_defaults
|
||||
def inject_toolbar_routes_host_if_required(
|
||||
endpoint: str, values: dict[str, t.Any]
|
||||
) -> None:
|
||||
if app.url_map.is_endpoint_expecting(endpoint, "toolbar_routes_host"):
|
||||
values.setdefault("toolbar_routes_host", request.host)
|
||||
|
||||
# Automatically strip `toolbar_routes_host` from the endpoint values so
|
||||
# that the `send_static_host` method doesn't receive that parameter,
|
||||
# as it's not actually required internally.
|
||||
@app.url_value_preprocessor
|
||||
def strip_toolbar_routes_host_from_static_endpoint(
|
||||
endpoint: str | None, values: dict[str, t.Any] | None
|
||||
) -> None:
|
||||
if (
|
||||
endpoint
|
||||
and values
|
||||
and app.url_map.is_endpoint_expecting(
|
||||
endpoint, "toolbar_routes_host"
|
||||
)
|
||||
):
|
||||
values.pop("toolbar_routes_host", None)
|
||||
|
||||
self.toolbar_routes_host = toolbar_routes_host
|
||||
|
||||
def dispatch_request(self) -> t.Any:
|
||||
"""Modified version of ``Flask.dispatch_request`` to call
|
||||
:meth:`process_view`.
|
||||
"""
|
||||
# self references this extension, use current_app to call app methods.
|
||||
app = current_app._get_current_object() # type: ignore[attr-defined]
|
||||
req = request_ctx.request
|
||||
app = current_app
|
||||
|
||||
if req.routing_exception is not None:
|
||||
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
|
||||
# request came with the OPTIONS method, reply automatically
|
||||
if getattr(rule, 'provide_automatic_options', False) \
|
||||
and req.method == 'OPTIONS':
|
||||
if (
|
||||
getattr(rule, "provide_automatic_options", False)
|
||||
and req.method == "OPTIONS"
|
||||
):
|
||||
return app.make_default_options_response()
|
||||
|
||||
# otherwise dispatch to the handler for that endpoint
|
||||
view_func = app.view_functions[rule.endpoint]
|
||||
view_func = self.process_view(app, view_func, req.view_args)
|
||||
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):
|
||||
def _show_toolbar(self) -> bool:
|
||||
"""Return a boolean to indicate if we need to show the toolbar."""
|
||||
if request.blueprint == 'debugtoolbar':
|
||||
if request.blueprint == "debugtoolbar":
|
||||
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:
|
||||
return False
|
||||
|
||||
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."""
|
||||
return send_from_directory(self._static_dir, filename)
|
||||
|
||||
def process_request(self):
|
||||
def process_request(self) -> None:
|
||||
g.debug_toolbar = self
|
||||
|
||||
if not self._show_toolbar():
|
||||
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.get()[real_request] = (
|
||||
DebugToolbar(real_request, self.jinja_env))
|
||||
self.debug_toolbars_var.get()[real_request] = DebugToolbar(
|
||||
real_request, self.jinja_env
|
||||
)
|
||||
|
||||
for panel in self.debug_toolbars_var.get()[real_request].panels:
|
||||
panel.process_request(real_request)
|
||||
|
||||
def process_view(self, app, view_func, view_kwargs):
|
||||
""" This method is called just before the flask view is called.
|
||||
def process_view(
|
||||
self,
|
||||
app: Flask,
|
||||
view_func: c.Callable[..., t.Any],
|
||||
view_kwargs: dict[str, t.Any],
|
||||
) -> c.Callable[..., t.Any]:
|
||||
"""This method is called just before the flask view is called.
|
||||
This is done by the dispatch_request method.
|
||||
"""
|
||||
real_request = request._get_current_object()
|
||||
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||
|
||||
try:
|
||||
toolbar = self.debug_toolbars_var.get({})[real_request]
|
||||
except KeyError:
|
||||
@@ -189,57 +270,65 @@ class DebugToolbarExtension(object):
|
||||
|
||||
for panel in toolbar.panels:
|
||||
new_view = panel.process_view(real_request, view_func, view_kwargs)
|
||||
|
||||
if new_view:
|
||||
view_func = new_view
|
||||
|
||||
return view_func
|
||||
|
||||
def process_response(self, response):
|
||||
real_request = request._get_current_object()
|
||||
def process_response(self, response: Response) -> Response:
|
||||
real_request = request._get_current_object() # type: ignore[attr-defined]
|
||||
|
||||
if real_request not in self.debug_toolbars_var.get({}):
|
||||
return response
|
||||
|
||||
# Intercept http redirect codes and display an html page with a
|
||||
# link to the target.
|
||||
if current_app.config['DEBUG_TB_INTERCEPT_REDIRECTS']:
|
||||
if current_app.config["DEBUG_TB_INTERCEPT_REDIRECTS"]:
|
||||
if response.status_code in self._redirect_codes:
|
||||
redirect_to = response.location
|
||||
redirect_code = response.status_code
|
||||
|
||||
if redirect_to:
|
||||
content = self.render('redirect.html', {
|
||||
'redirect_to': redirect_to,
|
||||
'redirect_code': redirect_code
|
||||
})
|
||||
content = self.render(
|
||||
"redirect.html",
|
||||
{"redirect_to": redirect_to, "redirect_code": redirect_code},
|
||||
)
|
||||
response.content_length = len(content)
|
||||
response.location = None
|
||||
del response.location
|
||||
response.response = [content]
|
||||
response.status_code = 200
|
||||
|
||||
# If the http response code is an allowed code then we process to add the
|
||||
# toolbar to the returned html response.
|
||||
if not (response.status_code in self._toolbar_codes and
|
||||
response.is_sequence and
|
||||
response.headers['content-type'].startswith('text/html')):
|
||||
if not (
|
||||
response.status_code in self._toolbar_codes
|
||||
and response.is_sequence
|
||||
and response.headers["content-type"].startswith("text/html")
|
||||
):
|
||||
return response
|
||||
|
||||
content_encoding = response.headers.get('Content-Encoding')
|
||||
if content_encoding and 'gzip' in content_encoding:
|
||||
content_encoding = response.headers.get("Content-Encoding")
|
||||
|
||||
if content_encoding and "gzip" in content_encoding:
|
||||
response_html = gzip_decompress(response.data).decode()
|
||||
else:
|
||||
response_html = response.get_data(as_text=True)
|
||||
|
||||
no_case = response_html.lower()
|
||||
body_end = no_case.rfind('</body>')
|
||||
body_end = no_case.rfind("</body>")
|
||||
|
||||
if body_end >= 0:
|
||||
before = response_html[:body_end]
|
||||
after = response_html[body_end:]
|
||||
elif no_case.startswith('<!doctype html>'):
|
||||
elif no_case.startswith("<!doctype html>"):
|
||||
before = response_html
|
||||
after = ''
|
||||
after = ""
|
||||
else:
|
||||
warnings.warn('Could not insert debug toolbar.'
|
||||
' </body> tag not found in response.')
|
||||
warnings.warn(
|
||||
"Could not insert debug toolbar." " </body> tag not found in response.",
|
||||
stacklevel=1,
|
||||
)
|
||||
return response
|
||||
|
||||
toolbar = self.debug_toolbars_var.get()[real_request]
|
||||
@@ -249,19 +338,22 @@ class DebugToolbarExtension(object):
|
||||
|
||||
toolbar_html = toolbar.render_toolbar()
|
||||
|
||||
content = ''.join((before, toolbar_html, after))
|
||||
content = content.encode('utf-8')
|
||||
if content_encoding and 'gzip' in content_encoding:
|
||||
content = gzip_compress(content)
|
||||
response.response = [content]
|
||||
response.content_length = len(content)
|
||||
content = "".join((before, toolbar_html, after))
|
||||
content_bytes = content.encode("utf-8")
|
||||
|
||||
if content_encoding and "gzip" in content_encoding:
|
||||
content_bytes = gzip_compress(content_bytes)
|
||||
|
||||
response.response = [content_bytes]
|
||||
response.content_length = len(content_bytes)
|
||||
|
||||
return response
|
||||
|
||||
def teardown_request(self, exc):
|
||||
def teardown_request(self, exc: BaseException | None) -> None:
|
||||
# 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)
|
||||
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):
|
||||
"""
|
||||
Base class for debug panels.
|
||||
"""
|
||||
# name = Base
|
||||
class DebugPanel:
|
||||
"""Base class for debug panels."""
|
||||
|
||||
name: str
|
||||
|
||||
# If content returns something, set to true in subclass
|
||||
has_content = False
|
||||
@@ -15,18 +22,21 @@ class DebugPanel(object):
|
||||
|
||||
# We'll maintain a local context instance so we can expose our template
|
||||
# context variables to panels which need them:
|
||||
context = {}
|
||||
context: dict[str, t.Any] = {}
|
||||
|
||||
# Panel methods
|
||||
def __init__(self, jinja_env, context={}):
|
||||
self.context.update(context)
|
||||
self.jinja_env = jinja_env
|
||||
def __init__(
|
||||
self, jinja_env: Environment, context: dict[str, t.Any] | None = None
|
||||
) -> None:
|
||||
if context is not None:
|
||||
self.context.update(context)
|
||||
|
||||
self.jinja_env = jinja_env
|
||||
# If the client enabled the panel
|
||||
self.is_active = False
|
||||
|
||||
@classmethod
|
||||
def init_app(cls, app):
|
||||
def init_app(cls, app: Flask) -> None:
|
||||
"""Method that can be overridden by child classes.
|
||||
Can be used for setting up additional URL-rules/routes.
|
||||
|
||||
@@ -48,37 +58,42 @@ class DebugPanel(object):
|
||||
"""
|
||||
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)
|
||||
return template.render(**context)
|
||||
|
||||
def dom_id(self):
|
||||
return 'flDebug%sPanel' % (self.name.replace(' ', ''))
|
||||
def dom_id(self) -> str:
|
||||
return f"flDebug{self.name.replace(' ', '')}Panel"
|
||||
|
||||
def nav_title(self):
|
||||
def nav_title(self) -> str:
|
||||
"""Title showing in toolbar"""
|
||||
raise NotImplementedError
|
||||
|
||||
def nav_subtitle(self):
|
||||
def nav_subtitle(self) -> str:
|
||||
"""Subtitle showing until title in toolbar"""
|
||||
return ''
|
||||
return ""
|
||||
|
||||
def title(self):
|
||||
def title(self) -> str:
|
||||
"""Title showing in panel"""
|
||||
raise NotImplementedError
|
||||
|
||||
def url(self):
|
||||
def url(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def content(self):
|
||||
def content(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
# Standard middleware methods
|
||||
def process_request(self, request):
|
||||
def process_request(self, request: Request) -> None:
|
||||
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
|
||||
|
||||
def process_response(self, request, response):
|
||||
def process_response(self, request: Request, response: Response) -> None:
|
||||
pass
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
from flask import current_app
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from __future__ import annotations
|
||||
|
||||
_ = lambda x: x
|
||||
from flask import current_app
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class ConfigVarsDebugPanel(DebugPanel):
|
||||
"""
|
||||
A panel to display all variables from Flask configuration
|
||||
"""
|
||||
name = 'ConfigVars'
|
||||
"""A panel to display all variables from Flask configuration."""
|
||||
|
||||
name = "ConfigVars"
|
||||
has_content = True
|
||||
|
||||
def nav_title(self):
|
||||
return _('Config')
|
||||
def nav_title(self) -> str:
|
||||
return "Config"
|
||||
|
||||
def title(self):
|
||||
return _('Config')
|
||||
def title(self) -> str:
|
||||
return "Config"
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self):
|
||||
def content(self) -> str:
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'config': current_app.config,
|
||||
})
|
||||
|
||||
return self.render('panels/config_vars.html', context)
|
||||
context.update(
|
||||
{
|
||||
"config": current_app.config,
|
||||
}
|
||||
)
|
||||
return self.render("panels/config_vars.html", context)
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
from flask import g
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from __future__ import annotations
|
||||
|
||||
_ = lambda x: x
|
||||
from flask import g
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class GDebugPanel(DebugPanel):
|
||||
"""
|
||||
A panel to display flask.g content.
|
||||
"""
|
||||
name = 'g'
|
||||
"""A panel to display ``flask.g`` content."""
|
||||
|
||||
name = "g"
|
||||
has_content = True
|
||||
|
||||
def nav_title(self):
|
||||
return _('flask.g')
|
||||
def nav_title(self) -> str:
|
||||
return "flask.g"
|
||||
|
||||
def title(self):
|
||||
return _('flask.g content')
|
||||
def title(self) -> str:
|
||||
return "flask.g content"
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self):
|
||||
def content(self) -> str:
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'g_content': g.__dict__
|
||||
})
|
||||
return self.render('panels/g.html', context)
|
||||
context.update({"g_content": g.__dict__})
|
||||
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):
|
||||
"""
|
||||
A panel to display HTTP headers.
|
||||
"""
|
||||
name = 'Header'
|
||||
"""A panel to display HTTP headers."""
|
||||
|
||||
name = "Header"
|
||||
has_content = True
|
||||
# List of headers we want to display
|
||||
header_filter = (
|
||||
'CONTENT_TYPE',
|
||||
'HTTP_ACCEPT',
|
||||
'HTTP_ACCEPT_CHARSET',
|
||||
'HTTP_ACCEPT_ENCODING',
|
||||
'HTTP_ACCEPT_LANGUAGE',
|
||||
'HTTP_CACHE_CONTROL',
|
||||
'HTTP_CONNECTION',
|
||||
'HTTP_HOST',
|
||||
'HTTP_KEEP_ALIVE',
|
||||
'HTTP_REFERER',
|
||||
'HTTP_USER_AGENT',
|
||||
'QUERY_STRING',
|
||||
'REMOTE_ADDR',
|
||||
'REMOTE_HOST',
|
||||
'REQUEST_METHOD',
|
||||
'SCRIPT_NAME',
|
||||
'SERVER_NAME',
|
||||
'SERVER_PORT',
|
||||
'SERVER_PROTOCOL',
|
||||
'SERVER_SOFTWARE',
|
||||
header_filter: tuple[str, ...] = (
|
||||
"CONTENT_TYPE",
|
||||
"HTTP_ACCEPT",
|
||||
"HTTP_ACCEPT_CHARSET",
|
||||
"HTTP_ACCEPT_ENCODING",
|
||||
"HTTP_ACCEPT_LANGUAGE",
|
||||
"HTTP_CACHE_CONTROL",
|
||||
"HTTP_CONNECTION",
|
||||
"HTTP_HOST",
|
||||
"HTTP_KEEP_ALIVE",
|
||||
"HTTP_REFERER",
|
||||
"HTTP_USER_AGENT",
|
||||
"QUERY_STRING",
|
||||
"REMOTE_ADDR",
|
||||
"REMOTE_HOST",
|
||||
"REQUEST_METHOD",
|
||||
"SCRIPT_NAME",
|
||||
"SERVER_NAME",
|
||||
"SERVER_PORT",
|
||||
"SERVER_PROTOCOL",
|
||||
"SERVER_SOFTWARE",
|
||||
)
|
||||
|
||||
def nav_title(self):
|
||||
return _('HTTP Headers')
|
||||
def nav_title(self) -> str:
|
||||
return "HTTP Headers"
|
||||
|
||||
def title(self):
|
||||
return _('HTTP Headers')
|
||||
def title(self) -> str:
|
||||
return "HTTP Headers"
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def process_request(self, request):
|
||||
self.headers = dict(
|
||||
[(k, request.environ[k])
|
||||
for k in self.header_filter if k in request.environ]
|
||||
)
|
||||
def process_request(self, request: Request) -> None:
|
||||
self.headers: dict[str, t.Any] = {
|
||||
k: request.environ[k] for k in self.header_filter if k in request.environ
|
||||
}
|
||||
|
||||
def content(self):
|
||||
def content(self) -> str:
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'headers': self.headers
|
||||
})
|
||||
return self.render('panels/headers.html', context)
|
||||
context.update({"headers": self.headers})
|
||||
return self.render("panels/headers.html", context)
|
||||
|
||||
@@ -1,55 +1,57 @@
|
||||
from __future__ import with_statement
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
try:
|
||||
import threading
|
||||
except ImportError:
|
||||
threading = None
|
||||
import threading
|
||||
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from flask_debugtoolbar.utils import format_fname
|
||||
from werkzeug import Request
|
||||
|
||||
_ = lambda x: x
|
||||
from ..utils import format_fname
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class ThreadTrackingHandler(logging.Handler):
|
||||
def __init__(self):
|
||||
if threading is None:
|
||||
raise NotImplementedError("threading module is not available, \
|
||||
the logging panel cannot be used without it")
|
||||
logging.Handler.__init__(self)
|
||||
self.records = {} # a dictionary that maps threads to log records
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
# a dictionary that maps threads to log records
|
||||
self.records: dict[threading.Thread, list[logging.LogRecord]] = {}
|
||||
|
||||
def emit(self, record):
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
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
|
||||
provided, returns a list for the current thread.
|
||||
"""
|
||||
if thread is None:
|
||||
thread = threading.current_thread()
|
||||
|
||||
if thread not in self.records:
|
||||
self.records[thread] = []
|
||||
|
||||
return self.records[thread]
|
||||
|
||||
def clear_records(self, thread=None):
|
||||
def clear_records(self, thread: threading.Thread | None = None) -> None:
|
||||
if thread is None:
|
||||
thread = threading.current_thread()
|
||||
|
||||
if thread in self.records:
|
||||
del self.records[thread]
|
||||
|
||||
|
||||
handler = None
|
||||
handler: ThreadTrackingHandler = None # type: ignore[assignment]
|
||||
_init_lock = threading.Lock()
|
||||
|
||||
|
||||
def _init_once():
|
||||
def _init_once() -> None:
|
||||
global handler
|
||||
|
||||
if handler is not None:
|
||||
return
|
||||
|
||||
with _init_lock:
|
||||
if handler is not None:
|
||||
return
|
||||
@@ -59,57 +61,55 @@ def _init_once():
|
||||
# and not configure console logging for the request log.
|
||||
# Werkzeug's default log level is INFO so this message probably won't
|
||||
# be seen.
|
||||
try:
|
||||
from werkzeug._internal import _log
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
_log('debug', 'Initializing Flask-DebugToolbar log handler')
|
||||
from werkzeug._internal import _log
|
||||
|
||||
_log("debug", "Initializing Flask-DebugToolbar log handler")
|
||||
handler = ThreadTrackingHandler()
|
||||
logging.root.addHandler(handler)
|
||||
|
||||
|
||||
class LoggingPanel(DebugPanel):
|
||||
name = 'Logging'
|
||||
name = "Logging"
|
||||
has_content = True
|
||||
|
||||
def process_request(self, request):
|
||||
def process_request(self, request: Request) -> None:
|
||||
_init_once()
|
||||
handler.clear_records()
|
||||
|
||||
def get_and_delete(self):
|
||||
def get_and_delete(self) -> list[logging.LogRecord]:
|
||||
records = handler.get_records()
|
||||
handler.clear_records()
|
||||
return records
|
||||
|
||||
def nav_title(self):
|
||||
return _("Logging")
|
||||
def nav_title(self) -> str:
|
||||
return "Logging"
|
||||
|
||||
def nav_subtitle(self):
|
||||
# FIXME l10n: use ngettext
|
||||
def nav_subtitle(self) -> str:
|
||||
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):
|
||||
return _('Log Messages')
|
||||
def title(self) -> str:
|
||||
return "Log Messages"
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self):
|
||||
def content(self) -> str:
|
||||
records = []
|
||||
|
||||
for record in self.get_and_delete():
|
||||
records.append({
|
||||
'message': record.getMessage(),
|
||||
'time': datetime.datetime.fromtimestamp(record.created),
|
||||
'level': record.levelname,
|
||||
'file': format_fname(record.pathname),
|
||||
'file_long': record.pathname,
|
||||
'line': record.lineno,
|
||||
})
|
||||
records.append(
|
||||
{
|
||||
"message": record.getMessage(),
|
||||
"time": datetime.datetime.fromtimestamp(record.created),
|
||||
"level": record.levelname,
|
||||
"file": format_fname(record.pathname),
|
||||
"file_long": record.pathname,
|
||||
"line": record.lineno,
|
||||
}
|
||||
)
|
||||
|
||||
context = self.context.copy()
|
||||
context.update({'records': records})
|
||||
|
||||
return self.render('panels/logger.html', context)
|
||||
context.update({"records": records})
|
||||
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:
|
||||
import cProfile as profile
|
||||
except ImportError:
|
||||
import profile
|
||||
import functools
|
||||
import pstats
|
||||
|
||||
from flask import current_app
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from flask_debugtoolbar.utils import format_fname
|
||||
import profile # type: ignore[no-redef]
|
||||
|
||||
|
||||
class ProfilerDebugPanel(DebugPanel):
|
||||
"""
|
||||
Panel that displays the time a response took with cProfile output.
|
||||
"""
|
||||
|
||||
name = 'Profiler'
|
||||
"""Panel that displays the time a response took with cProfile output."""
|
||||
|
||||
name = "Profiler"
|
||||
user_activate = True
|
||||
|
||||
def __init__(self, jinja_env, context={}):
|
||||
DebugPanel.__init__(self, jinja_env, context=context)
|
||||
if current_app.config.get('DEBUG_TB_PROFILER_ENABLED'):
|
||||
is_active: bool = False
|
||||
dump_filename: str | None = None
|
||||
profiler: profile.Profile
|
||||
stats: pstats.Stats | None = None
|
||||
function_calls: list[dict[str, t.Any]]
|
||||
|
||||
def __init__(
|
||||
self, jinja_env: Environment, context: dict[str, t.Any] | None = None
|
||||
) -> None:
|
||||
super().__init__(jinja_env, context=context)
|
||||
|
||||
self.dump_filename = None
|
||||
|
||||
if current_app.config.get("DEBUG_TB_PROFILER_ENABLED"):
|
||||
self.is_active = True
|
||||
self.dump_filename = current_app.config.get(
|
||||
"DEBUG_TB_PROFILER_DUMP_FILENAME"
|
||||
)
|
||||
|
||||
def has_content(self):
|
||||
@property
|
||||
def has_content(self) -> bool: # type: ignore[override]
|
||||
return bool(self.profiler)
|
||||
|
||||
def process_request(self, request):
|
||||
def process_request(self, request: Request) -> None:
|
||||
if not self.is_active:
|
||||
return
|
||||
|
||||
self.profiler = profile.Profile()
|
||||
self.profiler = profile.Profile() # pyright: ignore
|
||||
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:
|
||||
func = functools.partial(self.profiler.runcall, view_func)
|
||||
functools.update_wrapper(func, view_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:
|
||||
return False
|
||||
return
|
||||
|
||||
if self.profiler is not None:
|
||||
self.profiler.disable()
|
||||
self.profiler.disable() # pyright: ignore
|
||||
|
||||
try:
|
||||
stats = pstats.Stats(self.profiler)
|
||||
except TypeError:
|
||||
self.is_active = False
|
||||
return False
|
||||
function_calls = []
|
||||
for func in stats.sort_stats(1).fcn_list:
|
||||
current = {}
|
||||
info = stats.stats[func]
|
||||
return
|
||||
|
||||
function_calls: list[dict[str, t.Any]] = []
|
||||
|
||||
for func in stats.sort_stats(1).fcn_list: # type: ignore[attr-defined]
|
||||
current: dict[str, t.Any] = {}
|
||||
info = stats.stats[func] # type: ignore[attr-defined]
|
||||
|
||||
# Number of calls
|
||||
if info[0] != info[1]:
|
||||
current['ncalls'] = '%d/%d' % (info[1], info[0])
|
||||
current["ncalls"] = f"{info[1]}/{info[0]}"
|
||||
else:
|
||||
current['ncalls'] = info[1]
|
||||
current["ncalls"] = info[1]
|
||||
|
||||
# Total time
|
||||
current['tottime'] = info[2] * 1000
|
||||
current["tottime"] = info[2] * 1000
|
||||
|
||||
# Quotient of total time divided by number of calls
|
||||
if info[1]:
|
||||
current['percall'] = info[2] * 1000 / info[1]
|
||||
current["percall"] = info[2] * 1000 / info[1]
|
||||
else:
|
||||
current['percall'] = 0
|
||||
current["percall"] = 0
|
||||
|
||||
# Cumulative time
|
||||
current['cumtime'] = info[3] * 1000
|
||||
current["cumtime"] = info[3] * 1000
|
||||
|
||||
# Quotient of the cumulative time divided by the number of
|
||||
# primitive calls.
|
||||
if info[0]:
|
||||
current['percall_cum'] = info[3] * 1000 / info[0]
|
||||
current["percall_cum"] = info[3] * 1000 / info[0]
|
||||
else:
|
||||
current['percall_cum'] = 0
|
||||
current["percall_cum"] = 0
|
||||
|
||||
# Filename
|
||||
filename = pstats.func_std_string(func)
|
||||
current['filename_long'] = filename
|
||||
current['filename'] = format_fname(filename)
|
||||
filename = pstats.func_std_string(func) # type: ignore[attr-defined]
|
||||
current["filename_long"] = filename
|
||||
current["filename"] = format_fname(filename)
|
||||
function_calls.append(current)
|
||||
|
||||
self.stats = stats
|
||||
@@ -98,32 +126,33 @@ class ProfilerDebugPanel(DebugPanel):
|
||||
filename = self.dump_filename()
|
||||
else:
|
||||
filename = self.dump_filename
|
||||
|
||||
self.profiler.dump_stats(filename)
|
||||
|
||||
return response
|
||||
|
||||
def title(self):
|
||||
def title(self) -> str:
|
||||
if not self.is_active:
|
||||
return "Profiler not active"
|
||||
return 'View: %.2fms' % (float(self.stats.total_tt) * 1000,)
|
||||
|
||||
def nav_title(self):
|
||||
return 'Profiler'
|
||||
return f"View: {float(self.stats.total_tt) * 1000:.2f}ms" # type: ignore[union-attr]
|
||||
|
||||
def nav_subtitle(self):
|
||||
def nav_title(self) -> str:
|
||||
return "Profiler"
|
||||
|
||||
def nav_subtitle(self) -> str:
|
||||
if not self.is_active:
|
||||
return "in-active"
|
||||
return 'View: %.2fms' % (float(self.stats.total_tt) * 1000,)
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
return f"View: {float(self.stats.total_tt) * 1000:.2f}ms" # type: ignore[union-attr]
|
||||
|
||||
def content(self):
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self) -> str:
|
||||
if not self.is_active:
|
||||
return "The profiler is not activated, activate it to use it"
|
||||
|
||||
context = {
|
||||
'stats': self.stats,
|
||||
'function_calls': self.function_calls,
|
||||
"stats": self.stats,
|
||||
"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 werkzeug import Request
|
||||
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
|
||||
_ = lambda x: x
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class RequestVarsDebugPanel(DebugPanel):
|
||||
"""
|
||||
A panel to display request variables (POST/GET, session, cookies).
|
||||
"""
|
||||
name = 'RequestVars'
|
||||
"""A panel to display request variables (POST/GET, session, cookies)."""
|
||||
|
||||
name = "RequestVars"
|
||||
has_content = True
|
||||
|
||||
def nav_title(self):
|
||||
return _('Request Vars')
|
||||
def nav_title(self) -> str:
|
||||
return "Request Vars"
|
||||
|
||||
def title(self):
|
||||
return _('Request Vars')
|
||||
def title(self) -> str:
|
||||
return "Request Vars"
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def process_request(self, request):
|
||||
def process_request(self, request: Request) -> None:
|
||||
self.request = request
|
||||
self.session = session
|
||||
self.view_func = None
|
||||
self.view_args = []
|
||||
self.view_kwargs = {}
|
||||
self.view_func: c.Callable[..., t.Any] | None = None
|
||||
self.view_kwargs: dict[str, t.Any] = {}
|
||||
|
||||
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_kwargs = view_kwargs
|
||||
|
||||
def content(self):
|
||||
def content(self) -> str:
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'get': self.request.args.lists(),
|
||||
'post': self.request.form.lists(),
|
||||
'cookies': self.request.cookies.items(),
|
||||
'view_func': ('%s.%s' % (self.view_func.__module__,
|
||||
self.view_func.__name__)
|
||||
if self.view_func else '[unknown]'),
|
||||
'view_args': self.view_args,
|
||||
'view_kwargs': self.view_kwargs or {},
|
||||
'session': self.session.items(),
|
||||
})
|
||||
context.update(
|
||||
{
|
||||
"get": self.request.args.lists(),
|
||||
"post": self.request.form.lists(),
|
||||
"cookies": self.request.cookies.items(),
|
||||
"view_func": (
|
||||
f"{self.view_func.__module__}.{self.view_func.__name__}"
|
||||
if self.view_func
|
||||
else "[unknown]"
|
||||
),
|
||||
"view_kwargs": self.view_kwargs or {},
|
||||
"session": self.session.items(),
|
||||
}
|
||||
)
|
||||
|
||||
return self.render('panels/request_vars.html', context)
|
||||
return self.render("panels/request_vars.html", context)
|
||||
|
||||
@@ -1,38 +1,44 @@
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from flask import current_app
|
||||
from __future__ import annotations
|
||||
|
||||
_ = lambda x: x
|
||||
from flask import current_app
|
||||
from werkzeug import Request
|
||||
from werkzeug.routing import Rule
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class RouteListDebugPanel(DebugPanel):
|
||||
"""
|
||||
Panel that displays the URL routing rules.
|
||||
"""
|
||||
name = 'RouteList'
|
||||
"""Panel that displays the URL routing rules."""
|
||||
|
||||
name = "RouteList"
|
||||
has_content = True
|
||||
routes = []
|
||||
routes: list[Rule] = []
|
||||
|
||||
def nav_title(self):
|
||||
return _('Route List')
|
||||
def nav_title(self) -> str:
|
||||
return "Route List"
|
||||
|
||||
def title(self):
|
||||
return _('Route List')
|
||||
def title(self) -> str:
|
||||
return "Route List"
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def nav_subtitle(self):
|
||||
def nav_subtitle(self) -> str:
|
||||
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 = [
|
||||
rule
|
||||
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):
|
||||
return self.render('panels/route_list.html', {
|
||||
'routes': self.routes,
|
||||
})
|
||||
def content(self) -> str:
|
||||
return self.render(
|
||||
"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:
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
except ImportError:
|
||||
sqlalchemy_available = False
|
||||
get_recorded_queries = SQLAlchemy = None
|
||||
debug_enables_record_queries = False
|
||||
sqlalchemy_available: bool = False
|
||||
get_recorded_queries = SQLAlchemy = None # type: ignore[misc, assignment]
|
||||
debug_enables_record_queries: bool = False
|
||||
else:
|
||||
try:
|
||||
from flask_sqlalchemy.record_queries import get_recorded_queries
|
||||
from flask_sqlalchemy.record_queries import ( # type: ignore[assignment]
|
||||
get_recorded_queries,
|
||||
)
|
||||
|
||||
debug_enables_record_queries = False
|
||||
except ImportError:
|
||||
# For flask_sqlalchemy < 3.0.0
|
||||
from flask_sqlalchemy import 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
|
||||
# SQLALCHEMY_RECORD_QUERIES in debug or test mode
|
||||
debug_enables_record_queries = True
|
||||
|
||||
location_property = 'context'
|
||||
location_property: str = "context"
|
||||
else:
|
||||
location_property = 'location'
|
||||
location_property = "location"
|
||||
|
||||
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():
|
||||
return itsdangerous.URLSafeSerializer(current_app.config['SECRET_KEY'],
|
||||
salt='fdt-sql-query')
|
||||
def is_select(statement: str | bytes) -> bool:
|
||||
statement = statement.lower().strip()
|
||||
|
||||
if isinstance(statement, bytes):
|
||||
return statement.startswith(b"select")
|
||||
|
||||
return statement.startswith("select") # pyright: ignore
|
||||
|
||||
|
||||
def is_select(statement):
|
||||
prefix = b'select' if isinstance(statement, bytes) else 'select'
|
||||
return statement.lower().strip().startswith(prefix)
|
||||
|
||||
|
||||
def dump_query(statement, params):
|
||||
def dump_query(statement: str, params: t.Any) -> str | None:
|
||||
if not params or not is_select(statement):
|
||||
return None
|
||||
|
||||
@@ -50,9 +67,9 @@ def dump_query(statement, params):
|
||||
return None
|
||||
|
||||
|
||||
def load_query(data):
|
||||
def load_query(data: str) -> tuple[str, t.Any]:
|
||||
try:
|
||||
statement, params = query_signer().loads(request.args['query'])
|
||||
statement, params = query_signer().loads(data)
|
||||
except (itsdangerous.BadSignature, TypeError):
|
||||
abort(406)
|
||||
|
||||
@@ -63,22 +80,21 @@ def load_query(data):
|
||||
return statement, params
|
||||
|
||||
|
||||
def extension_used():
|
||||
return 'sqlalchemy' in current_app.extensions
|
||||
def extension_used() -> bool:
|
||||
return "sqlalchemy" in current_app.extensions
|
||||
|
||||
|
||||
def recording_enabled():
|
||||
def recording_enabled() -> bool:
|
||||
return (
|
||||
(debug_enables_record_queries and current_app.debug) or
|
||||
current_app.config.get('SQLALCHEMY_RECORD_QUERIES')
|
||||
)
|
||||
debug_enables_record_queries and current_app.debug
|
||||
) 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()
|
||||
|
||||
|
||||
def get_queries():
|
||||
def get_queries() -> list[t.Any]:
|
||||
if get_recorded_queries:
|
||||
return get_recorded_queries()
|
||||
else:
|
||||
@@ -86,80 +102,85 @@ def get_queries():
|
||||
|
||||
|
||||
class SQLAlchemyDebugPanel(DebugPanel):
|
||||
"""
|
||||
Panel that displays the time a response took in milliseconds.
|
||||
"""
|
||||
name = 'SQLAlchemy'
|
||||
"""Panel that displays the time a response took in milliseconds."""
|
||||
|
||||
name = "SQLAlchemy"
|
||||
|
||||
@property
|
||||
def has_content(self):
|
||||
def has_content(self) -> bool: # type: ignore[override]
|
||||
return bool(get_queries()) or not is_available()
|
||||
|
||||
def process_request(self, request):
|
||||
pass
|
||||
def nav_title(self) -> str:
|
||||
return "SQLAlchemy"
|
||||
|
||||
def process_response(self, request, response):
|
||||
pass
|
||||
|
||||
def nav_title(self):
|
||||
return _('SQLAlchemy')
|
||||
|
||||
def nav_subtitle(self):
|
||||
def nav_subtitle(self) -> str:
|
||||
count = len(get_queries())
|
||||
|
||||
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):
|
||||
return _('SQLAlchemy queries')
|
||||
def title(self) -> str:
|
||||
return "SQLAlchemy queries"
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def content(self):
|
||||
def content(self) -> str:
|
||||
queries = get_queries()
|
||||
|
||||
if not queries and not is_available():
|
||||
return self.render('panels/sqlalchemy_error.html', {
|
||||
'sqlalchemy_available': sqlalchemy_available,
|
||||
'extension_used': extension_used(),
|
||||
'recording_enabled': recording_enabled(),
|
||||
})
|
||||
return self.render(
|
||||
"panels/sqlalchemy_error.html",
|
||||
{
|
||||
"sqlalchemy_available": sqlalchemy_available,
|
||||
"extension_used": extension_used(),
|
||||
"recording_enabled": recording_enabled(),
|
||||
},
|
||||
)
|
||||
|
||||
data = []
|
||||
|
||||
for query in queries:
|
||||
data.append({
|
||||
'duration': query.duration,
|
||||
'sql': format_sql(query.statement, query.parameters),
|
||||
'signed_query': dump_query(query.statement, query.parameters),
|
||||
'location_long': getattr(query, location_property),
|
||||
'location': format_fname(getattr(query, location_property))
|
||||
})
|
||||
return self.render('panels/sqlalchemy.html', {'queries': data})
|
||||
data.append(
|
||||
{
|
||||
"duration": query.duration,
|
||||
"sql": format_sql(query.statement, query.parameters),
|
||||
"signed_query": dump_query(query.statement, query.parameters),
|
||||
"location_long": getattr(query, location_property),
|
||||
"location": format_fname(getattr(query, location_property)),
|
||||
}
|
||||
)
|
||||
|
||||
return self.render("panels/sqlalchemy.html", {"queries": data})
|
||||
|
||||
|
||||
# Panel views
|
||||
|
||||
|
||||
@module.route('/sqlalchemy/sql_select', methods=['GET', 'POST'])
|
||||
@module.route('/sqlalchemy/sql_explain', methods=['GET', 'POST'],
|
||||
defaults=dict(explain=True))
|
||||
def sql_select(explain=False):
|
||||
statement, params = load_query(request.args['query'])
|
||||
engine = SQLAlchemy().get_engine(current_app)
|
||||
@module.route("/sqlalchemy/sql_select", methods=["GET", "POST"])
|
||||
@module.route(
|
||||
"/sqlalchemy/sql_explain", methods=["GET", "POST"], defaults=dict(explain=True)
|
||||
)
|
||||
def sql_select(explain: bool = False) -> str:
|
||||
statement, params = load_query(request.args["query"])
|
||||
engine = current_app.extensions["sqlalchemy"].engine
|
||||
|
||||
if explain:
|
||||
if engine.driver == 'pysqlite':
|
||||
statement = 'EXPLAIN QUERY PLAN\n%s' % statement
|
||||
if engine.driver == "pysqlite":
|
||||
statement = f"EXPLAIN QUERY PLAN\n{statement}"
|
||||
else:
|
||||
statement = 'EXPLAIN\n%s' % statement
|
||||
statement = f"EXPLAIN\n{statement}"
|
||||
|
||||
result = engine.execute(statement, params)
|
||||
return g.debug_toolbar.render('panels/sqlalchemy_select.html', {
|
||||
'result': result.fetchall(),
|
||||
'headers': result.keys(),
|
||||
'sql': format_sql(statement, params),
|
||||
'duration': float(request.args['duration']),
|
||||
})
|
||||
return g.debug_toolbar.render( # type: ignore[no-any-return]
|
||||
"panels/sqlalchemy_select.html",
|
||||
{
|
||||
"result": result.fetchall(),
|
||||
"headers": result.keys(),
|
||||
"sql": format_sql(statement, params),
|
||||
"duration": float(request.args["duration"]),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,136 +1,149 @@
|
||||
import collections
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import typing as t
|
||||
import uuid
|
||||
from collections import deque
|
||||
|
||||
from flask import (
|
||||
template_rendered, request, g,
|
||||
Response, current_app, abort, url_for
|
||||
)
|
||||
from flask_debugtoolbar import module
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from flask import request
|
||||
from flask import Response
|
||||
from flask import template_rendered
|
||||
from flask import url_for
|
||||
from jinja2 import Template
|
||||
|
||||
_ = lambda x: x
|
||||
from .. import module
|
||||
from . import DebugPanel
|
||||
|
||||
|
||||
class TemplateDebugPanel(DebugPanel):
|
||||
"""
|
||||
Panel that displays the time a response took in milliseconds.
|
||||
"""
|
||||
name = 'Template'
|
||||
"""Panel that displays the time a response took in milliseconds."""
|
||||
|
||||
name = "Template"
|
||||
has_content = True
|
||||
|
||||
# save the context for the 5 most recent requests
|
||||
template_cache = collections.deque(maxlen=5)
|
||||
template_cache: deque[tuple[str, list[dict[str, t.Any]]]] = deque(maxlen=5)
|
||||
|
||||
@classmethod
|
||||
def get_cache_for_key(self, key):
|
||||
for cache_key, value in self.template_cache:
|
||||
def get_cache_for_key(cls, key: str) -> list[dict[str, t.Any]]:
|
||||
for cache_key, value in cls.template_cache:
|
||||
if key == cache_key:
|
||||
return value
|
||||
|
||||
raise KeyError(key)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(self.__class__, self).__init__(*args, **kwargs)
|
||||
self.key = str(uuid.uuid4())
|
||||
self.templates = []
|
||||
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.key: str = str(uuid.uuid4())
|
||||
self.templates: list[dict[str, t.Any]] = []
|
||||
template_rendered.connect(self._store_template_info)
|
||||
|
||||
def _store_template_info(self, sender, **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
|
||||
# actually a template for this request
|
||||
if not self.templates and is_editor_enabled():
|
||||
self.template_cache.append((self.key, self.templates))
|
||||
|
||||
self.templates.append(kwargs)
|
||||
|
||||
def process_request(self, request):
|
||||
pass
|
||||
def nav_title(self) -> str:
|
||||
return "Templates"
|
||||
|
||||
def process_response(self, request, response):
|
||||
pass
|
||||
def nav_subtitle(self) -> str:
|
||||
return f"{len(self.templates)} rendered"
|
||||
|
||||
def nav_title(self):
|
||||
return _('Templates')
|
||||
def title(self) -> str:
|
||||
return "Templates"
|
||||
|
||||
def nav_subtitle(self):
|
||||
return "%d rendered" % len(self.templates)
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def title(self):
|
||||
return _('Templates')
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
|
||||
def content(self):
|
||||
return self.render('panels/template.html', {
|
||||
'key': self.key,
|
||||
'templates': self.templates,
|
||||
'editable': is_editor_enabled(),
|
||||
})
|
||||
def content(self) -> str:
|
||||
return self.render(
|
||||
"panels/template.html",
|
||||
{
|
||||
"key": self.key,
|
||||
"templates": self.templates,
|
||||
"editable": is_editor_enabled(),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def is_editor_enabled():
|
||||
return current_app.config.get('DEBUG_TB_TEMPLATE_EDITOR_ENABLED')
|
||||
def is_editor_enabled() -> bool:
|
||||
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():
|
||||
abort(403)
|
||||
|
||||
|
||||
def _get_source(template):
|
||||
with open(template.filename, 'rb') as fp:
|
||||
def _get_source(template: Template) -> str:
|
||||
if template.filename is None:
|
||||
return ""
|
||||
|
||||
with open(template.filename, "rb") as fp:
|
||||
source = fp.read()
|
||||
|
||||
return source.decode(_template_encoding())
|
||||
|
||||
|
||||
def _template_encoding():
|
||||
return getattr(current_app.jinja_loader, 'encoding', 'utf-8')
|
||||
def _template_encoding() -> str:
|
||||
return getattr(current_app.jinja_loader, "encoding", "utf-8")
|
||||
|
||||
|
||||
@module.route('/template/<key>')
|
||||
def template_editor(key):
|
||||
@module.route("/template/<key>")
|
||||
def template_editor(key: str) -> str:
|
||||
require_enabled()
|
||||
# TODO set up special loader that caches templates it loads
|
||||
# and can override template contents
|
||||
templates = [t['template'] for t in
|
||||
TemplateDebugPanel.get_cache_for_key(key)]
|
||||
return g.debug_toolbar.render('panels/template_editor.html', {
|
||||
'static_path': url_for('_debug_toolbar.static', filename=''),
|
||||
'request': request,
|
||||
'templates': [
|
||||
{'name': t.name, 'source': _get_source(t)}
|
||||
for t in templates
|
||||
]
|
||||
})
|
||||
templates = [t["template"] for t in TemplateDebugPanel.get_cache_for_key(key)]
|
||||
return g.debug_toolbar.render( # type: ignore[no-any-return]
|
||||
"panels/template_editor.html",
|
||||
{
|
||||
"static_path": url_for("_debug_toolbar.static", filename=""),
|
||||
"request": request,
|
||||
"templates": [
|
||||
{"name": t.name, "source": _get_source(t)} for t in templates
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@module.route('/template/<key>/save', methods=['POST'])
|
||||
def save_template(key):
|
||||
@module.route("/template/<key>/save", methods=["POST"])
|
||||
def save_template(key: str) -> str:
|
||||
require_enabled()
|
||||
template = TemplateDebugPanel.get_cache_for_key(key)[0]['template']
|
||||
content = request.form['content'].encode(_template_encoding())
|
||||
with open(template.filename, 'wb') as fp:
|
||||
template = TemplateDebugPanel.get_cache_for_key(key)[0]["template"]
|
||||
content = request.form["content"].encode(_template_encoding())
|
||||
|
||||
with open(template.filename, "wb") as fp:
|
||||
fp.write(content)
|
||||
return 'ok'
|
||||
|
||||
return "ok"
|
||||
|
||||
|
||||
@module.route('/template/<key>', methods=['POST'])
|
||||
def template_preview(key):
|
||||
@module.route("/template/<key>", methods=["POST"])
|
||||
def template_preview(key: str) -> str | Response:
|
||||
require_enabled()
|
||||
context = TemplateDebugPanel.get_cache_for_key(key)[0]['context']
|
||||
content = request.form['content']
|
||||
context = TemplateDebugPanel.get_cache_for_key(key)[0]["context"]
|
||||
content = request.form["content"]
|
||||
env = current_app.jinja_env.overlay(autoescape=True)
|
||||
|
||||
try:
|
||||
template = env.from_string(content)
|
||||
return template.render(context)
|
||||
except Exception as e:
|
||||
tb = sys.exc_info()[2]
|
||||
|
||||
try:
|
||||
while tb.tb_next:
|
||||
tb = tb.tb_next
|
||||
msg = {'lineno': tb.tb_lineno, 'error': str(e)}
|
||||
return Response(json.dumps(msg), status=400,
|
||||
mimetype='application/json')
|
||||
while tb.tb_next: # type: ignore[union-attr]
|
||||
tb = tb.tb_next # type: ignore[union-attr]
|
||||
|
||||
msg = {"lineno": tb.tb_lineno, "error": str(e)} # type: ignore[union-attr]
|
||||
return Response(json.dumps(msg), status=400, mimetype="application/json")
|
||||
finally:
|
||||
del tb
|
||||
|
||||
@@ -1,95 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from werkzeug import Request
|
||||
from werkzeug import Response
|
||||
|
||||
from . import DebugPanel
|
||||
|
||||
try:
|
||||
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):
|
||||
"""
|
||||
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
|
||||
"""Panel that displays the time a response took in milliseconds."""
|
||||
|
||||
def process_request(self, request):
|
||||
name = "Timer"
|
||||
has_content = HAVE_RESOURCE
|
||||
|
||||
def process_request(self, request: Request) -> None:
|
||||
self._start_time = time.time()
|
||||
if self.has_resource:
|
||||
|
||||
if HAVE_RESOURCE:
|
||||
self._start_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
|
||||
def process_response(self, request, response):
|
||||
self.total_time = (time.time() - self._start_time) * 1000
|
||||
if self.has_resource:
|
||||
def process_response(self, request: Request, response: Response) -> None:
|
||||
self.total_time: float = (time.time() - self._start_time) * 1000
|
||||
|
||||
if HAVE_RESOURCE:
|
||||
self._end_rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
|
||||
def nav_title(self):
|
||||
return _('Time')
|
||||
def nav_title(self) -> str:
|
||||
return "Time"
|
||||
|
||||
def nav_subtitle(self):
|
||||
# TODO l10n
|
||||
if not self.has_resource:
|
||||
return 'TOTAL: %0.2fms' % (self.total_time)
|
||||
def nav_subtitle(self) -> str:
|
||||
if not HAVE_RESOURCE:
|
||||
return f"TOTAL: {self.total_time:0.2f}ms"
|
||||
|
||||
utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime
|
||||
stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime
|
||||
return 'CPU: %0.2fms (%0.2fms)' % (
|
||||
(utime + stime) * 1000.0, self.total_time)
|
||||
return f"CPU: {(utime + stime) * 1000.0:0.2f}ms ({self.total_time:0.2f}ms)"
|
||||
|
||||
def title(self):
|
||||
return _('Resource Usage')
|
||||
def title(self) -> str:
|
||||
return "Resource Usage"
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def _elapsed_ru(self, name):
|
||||
return (getattr(self._end_rusage, name) - getattr(self._start_rusage, name))
|
||||
def _elapsed_ru(self, name: str) -> float:
|
||||
return getattr(self._end_rusage, name) - getattr(self._start_rusage, name) # type: ignore[no-any-return]
|
||||
|
||||
def content(self):
|
||||
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')
|
||||
stime = 1000 * self._elapsed_ru('ru_stime')
|
||||
vcsw = self._elapsed_ru('ru_nvcsw')
|
||||
ivcsw = self._elapsed_ru('ru_nivcsw')
|
||||
# minflt = self._elapsed_ru('ru_minflt')
|
||||
# majflt = self._elapsed_ru('ru_majflt')
|
||||
|
||||
# these are documented as not meaningful under Linux. If you're running BSD
|
||||
# feel free to enable them, and add any others that I hadn't gotten to before
|
||||
# I noticed that I was getting nothing but zeroes and that the docs agreed. :-(
|
||||
#
|
||||
# blkin = self._elapsed_ru('ru_inblock')
|
||||
# blkout = self._elapsed_ru('ru_oublock')
|
||||
# swap = self._elapsed_ru('ru_nswap')
|
||||
# rss = self._end_rusage.ru_maxrss
|
||||
# srss = self._end_rusage.ru_ixrss
|
||||
# urss = self._end_rusage.ru_idrss
|
||||
# usrss = self._end_rusage.ru_isrss
|
||||
|
||||
# TODO l10n on values
|
||||
# these are documented as not meaningful under Linux. If you're running BSD
|
||||
# feel free to enable them, and add any others that I hadn't gotten to before
|
||||
# I noticed that I was getting nothing but zeroes and that the docs agreed. :-(
|
||||
# blkin = self._elapsed_ru("ru_inblock")
|
||||
# blkout = self._elapsed_ru("ru_oublock")
|
||||
# swap = self._elapsed_ru("ru_nswap")
|
||||
# rss = self._end_rusage.ru_maxrss
|
||||
# srss = self._end_rusage.ru_ixrss
|
||||
# urss = self._end_rusage.ru_idrss
|
||||
# usrss = self._end_rusage.ru_isrss
|
||||
rows = (
|
||||
(_('User CPU time'), '%0.3f msec' % utime),
|
||||
(_('System CPU time'), '%0.3f msec' % stime),
|
||||
(_('Total CPU time'), '%0.3f msec' % (utime + stime)),
|
||||
(_('Elapsed time'), '%0.3f msec' % self.total_time),
|
||||
(_('Context switches'), '%d voluntary, %d involuntary' % (vcsw, ivcsw)),
|
||||
# ('Memory use', '%d max RSS, %d shared, %d unshared' % (rss, srss, urss + usrss)),
|
||||
# ('Page faults', '%d no i/o, %d requiring i/o' % (minflt, majflt)),
|
||||
# ('Disk operations', '%d in, %d out, %d swapout' % (blkin, blkout, swap)),
|
||||
("User CPU time", f"{utime:0.3f} msec"),
|
||||
("System CPU time", f"{stime:0.3f} msec"),
|
||||
("Total CPU time", f"{(utime + stime):0.3f} msec"),
|
||||
("Elapsed time", f"{self.total_time:0.3f} msec"),
|
||||
("Context switches", f"{vcsw} voluntary, {ivcsw} involuntary"),
|
||||
# (
|
||||
# "Memory use",
|
||||
# f"{rss} max RSS, {srss} shared, {urss + usrss} unshared",
|
||||
# ),
|
||||
# ("Page faults", f"{minflt} no i/o, {majflt} requiring i/o"),
|
||||
# ("Disk operations", f"{blkin} in, {blkout} out, {swap} swapout"),
|
||||
)
|
||||
|
||||
context = self.context.copy()
|
||||
context.update({
|
||||
'rows': rows,
|
||||
})
|
||||
|
||||
return self.render('panels/timer.html', context)
|
||||
context.update(
|
||||
{
|
||||
"rows": rows,
|
||||
}
|
||||
)
|
||||
return self.render("panels/timer.html", context)
|
||||
|
||||
@@ -1,51 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.metadata
|
||||
import os
|
||||
from sysconfig import get_path
|
||||
|
||||
from flask_debugtoolbar.panels import DebugPanel
|
||||
from . import DebugPanel
|
||||
|
||||
try:
|
||||
# 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
|
||||
flask_version: str = importlib.metadata.version("flask")
|
||||
|
||||
|
||||
class VersionDebugPanel(DebugPanel):
|
||||
"""
|
||||
Panel that displays the Flask version.
|
||||
"""
|
||||
name = 'Version'
|
||||
"""Panel that displays the Flask version."""
|
||||
|
||||
name = "Version"
|
||||
has_content = True
|
||||
|
||||
def nav_title(self):
|
||||
return _('Versions')
|
||||
def nav_title(self) -> str:
|
||||
return "Versions"
|
||||
|
||||
def nav_subtitle(self):
|
||||
return 'Flask %s' % flask_version
|
||||
def nav_subtitle(self) -> str:
|
||||
return f"Flask {flask_version}"
|
||||
|
||||
def url(self):
|
||||
return ''
|
||||
def url(self) -> str:
|
||||
return ""
|
||||
|
||||
def title(self):
|
||||
return _('Versions')
|
||||
def title(self) -> str:
|
||||
return "Versions"
|
||||
|
||||
def content(self):
|
||||
try:
|
||||
import importlib.metadata
|
||||
except ImportError:
|
||||
packages = []
|
||||
else:
|
||||
packages_metadata = [p.metadata for p in importlib.metadata.distributions()]
|
||||
packages = sorted(packages_metadata, key=lambda p: p['Name'].lower())
|
||||
|
||||
return self.render('panels/versions.html', {
|
||||
'packages': packages,
|
||||
'python_lib_dir': os.path.normpath(get_path('platlib')),
|
||||
})
|
||||
def content(self) -> str:
|
||||
packages_metadata = [p.metadata for p in importlib.metadata.distributions()]
|
||||
packages = sorted(packages_metadata, key=lambda p: p["Name"].lower())
|
||||
return self.render(
|
||||
"panels/versions.html",
|
||||
{
|
||||
"packages": packages,
|
||||
"python_lib_dir": os.path.normpath(get_path("platlib")),
|
||||
},
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
(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" />');
|
||||
var COOKIE_NAME = 'fldt';
|
||||
var COOKIE_NAME_ACTIVE = COOKIE_NAME +'_active';
|
||||
@@ -113,7 +113,7 @@
|
||||
});
|
||||
$(this).tablesorter({headers: headers});
|
||||
})
|
||||
.bind('sortEnd', function() {
|
||||
.on('sortEnd', function() {
|
||||
$(this).find('tbody tr').each(function(idx, elem) {
|
||||
var even = idx % 2 === 0;
|
||||
$(elem)
|
||||
@@ -143,7 +143,7 @@
|
||||
$('#flDebugToolbar').hide('fast');
|
||||
$('#flDebugToolbarHandle').show();
|
||||
// Unbind keydown
|
||||
$(document).unbind('keydown.flDebug');
|
||||
$(document).off('keydown.flDebug');
|
||||
if (setCookie) {
|
||||
$.cookie(COOKIE_NAME, 'hide', {
|
||||
path: '/',
|
||||
|
||||
@@ -4,14 +4,12 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>View Function</th>
|
||||
<th>args</th>
|
||||
<th>kwargs</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ view_func }}</td>
|
||||
<td>{{ view_args|default("None") }}</td>
|
||||
<td>
|
||||
{% if view_kwargs.items() %}
|
||||
{% for k, v in view_kwargs.items() %}
|
||||
|
||||
@@ -24,12 +24,6 @@
|
||||
<td>{{ package.get('Home-page') }}</td>
|
||||
<td>{{ package.get('Summary') }}</td>
|
||||
</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 %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,65 +1,68 @@
|
||||
try:
|
||||
from urllib.parse import unquote
|
||||
except ImportError:
|
||||
from urllib import unquote
|
||||
from __future__ import annotations
|
||||
|
||||
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 .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.request = request
|
||||
self.panels = []
|
||||
|
||||
self.template_context = {
|
||||
'static_path': url_for('_debug_toolbar.static', filename='')
|
||||
self.panels: list[DebugPanel] = []
|
||||
self.template_context: dict[str, t.Any] = {
|
||||
"static_path": url_for("_debug_toolbar.static", filename="")
|
||||
}
|
||||
|
||||
self.create_panels()
|
||||
|
||||
def create_panels(self):
|
||||
"""
|
||||
Populate debug panels
|
||||
"""
|
||||
activated = self.request.cookies.get('fldt_active', '')
|
||||
activated = unquote(activated).split(';')
|
||||
def create_panels(self) -> None:
|
||||
"""Populate debug panels"""
|
||||
activated_str = self.request.cookies.get("fldt_active", "")
|
||||
activated = unquote(activated_str).split(";")
|
||||
|
||||
for panel_class in self._iter_panels(current_app):
|
||||
panel_instance = panel_class(jinja_env=self.jinja_env,
|
||||
context=self.template_context)
|
||||
panel_instance = panel_class(
|
||||
jinja_env=self.jinja_env, context=self.template_context
|
||||
)
|
||||
|
||||
if panel_instance.dom_id() in activated:
|
||||
panel_instance.is_active = True
|
||||
|
||||
self.panels.append(panel_instance)
|
||||
|
||||
def render_toolbar(self):
|
||||
def render_toolbar(self) -> str:
|
||||
context = self.template_context.copy()
|
||||
context.update({'panels': self.panels})
|
||||
|
||||
template = self.jinja_env.get_template('base.html')
|
||||
context.update({"panels": self.panels})
|
||||
template = self.jinja_env.get_template("base.html")
|
||||
return template.render(**context)
|
||||
|
||||
@classmethod
|
||||
def load_panels(cls, app):
|
||||
def load_panels(cls, app: Flask) -> None:
|
||||
for panel_class in cls._iter_panels(app):
|
||||
# Call `.init_app()` on panels
|
||||
panel_class.init_app(app)
|
||||
|
||||
@classmethod
|
||||
def _iter_panels(cls, app):
|
||||
for panel_path in app.config['DEBUG_TB_PANELS']:
|
||||
def _iter_panels(cls, app: Flask) -> c.Iterator[type[DebugPanel]]:
|
||||
for panel_path in app.config["DEBUG_TB_PANELS"]:
|
||||
panel_class = cls._import_panel(app, panel_path)
|
||||
|
||||
if panel_class is not None:
|
||||
yield panel_class
|
||||
|
||||
@classmethod
|
||||
def _import_panel(cls, app, path):
|
||||
def _import_panel(cls, app: Flask, path: str) -> type[DebugPanel] | None:
|
||||
cache = cls._cached_panel_classes
|
||||
|
||||
try:
|
||||
@@ -68,9 +71,9 @@ class DebugToolbar(object):
|
||||
pass
|
||||
|
||||
try:
|
||||
panel_class = import_string(path)
|
||||
panel_class: type[DebugPanel] | None = import_string(path)
|
||||
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
|
||||
|
||||
cache[path] = panel_class
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections.abc as c
|
||||
import gzip
|
||||
import io
|
||||
import itertools
|
||||
import os.path
|
||||
import sys
|
||||
import io
|
||||
import gzip
|
||||
from types import ModuleType
|
||||
|
||||
from flask import current_app
|
||||
from markupsafe import Markup
|
||||
|
||||
try:
|
||||
from pygments import highlight
|
||||
from pygments.formatters import HtmlFormatter
|
||||
from pygments.lexers import SqlLexer
|
||||
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
|
||||
except ImportError:
|
||||
HAVE_PYGMENTS = False
|
||||
|
||||
try:
|
||||
import sqlparse
|
||||
import sqlparse # pyright: ignore
|
||||
|
||||
HAVE_SQLPARSE = True
|
||||
except ImportError:
|
||||
HAVE_SQLPARSE = False
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
|
||||
def format_fname(value):
|
||||
def format_fname(value: str) -> str:
|
||||
# If the value has a builtin prefix, return it unchanged
|
||||
if value.startswith(('{', '<')):
|
||||
if value.startswith(("{", "<")):
|
||||
return 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
|
||||
# to handle it as a project file
|
||||
if os.path.isabs(value):
|
||||
value = _shortest_relative_path(
|
||||
value, [current_app.root_path], os.path)
|
||||
value = _shortest_relative_path(value, [current_app.root_path], os.path)
|
||||
|
||||
# If the value is a relative path, it is a project file
|
||||
if not os.path.isabs(value):
|
||||
return os.path.join('.', value)
|
||||
return os.path.join(".", value)
|
||||
|
||||
# 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)
|
||||
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:
|
||||
try:
|
||||
relval = path_module.relpath(value, path)
|
||||
@@ -59,43 +67,45 @@ def _relative_paths(value, paths, path_module):
|
||||
# on Windows, relpath throws a ValueError for
|
||||
# paths with different drives
|
||||
continue
|
||||
|
||||
if not relval.startswith(path_module.pardir):
|
||||
yield relval
|
||||
|
||||
|
||||
def decode_text(value):
|
||||
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
|
||||
with a text-safe replacement for unrecognized characters.
|
||||
Unicode values are returned unchanged. Byte strings will be decoded
|
||||
with a text-safe replacement for unrecognized characters.
|
||||
"""
|
||||
if isinstance(value, bytes):
|
||||
return value.decode('ascii', 'replace')
|
||||
else:
|
||||
return value
|
||||
return value.decode("ascii", "replace")
|
||||
|
||||
return value # pyright: ignore
|
||||
|
||||
|
||||
def format_sql(query, args):
|
||||
def format_sql(query: str | bytes, args: object) -> str:
|
||||
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:
|
||||
return decode_text(query)
|
||||
|
||||
return Markup(highlight(
|
||||
query,
|
||||
SqlLexer(),
|
||||
HtmlFormatter(noclasses=True, style=PYGMENT_STYLE)))
|
||||
return Markup(
|
||||
highlight(query, 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()
|
||||
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)
|
||||
|
||||
return buff.getvalue()
|
||||
|
||||
|
||||
def gzip_decompress(data):
|
||||
with gzip.GzipFile(fileobj=io.BytesIO(data), mode='rb') as f:
|
||||
def gzip_decompress(data: bytes) -> bytes:
|
||||
with gzip.GzipFile(fileobj=io.BytesIO(data), mode="rb") as f:
|
||||
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]
|
||||
envlist =
|
||||
py3{12,11,10,9,8,7}
|
||||
stylecheck
|
||||
py3{12,11,10,9,8}
|
||||
minimal
|
||||
skip_missing_interpreters = True
|
||||
style
|
||||
typing
|
||||
docs
|
||||
skip_missing_interpreters = true
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
pytest
|
||||
Flask-SQLAlchemy
|
||||
Pygments
|
||||
commands =
|
||||
pytest
|
||||
package = wheel
|
||||
wheel_build_env = .pkg
|
||||
constrain_package_deps = true
|
||||
use_frozen_constraints = true
|
||||
deps = -r requirements/tests.txt
|
||||
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}
|
||||
|
||||
[testenv:minimal]
|
||||
deps =
|
||||
.
|
||||
commands =
|
||||
python -c "from flask_debugtoolbar import DebugToolbarExtension"
|
||||
commands = python -c "from flask_debugtoolbar import DebugToolbarExtension"
|
||||
|
||||
[testenv:stylecheck]
|
||||
deps =
|
||||
pycodestyle
|
||||
commands =
|
||||
# E731: do not assign a lambda expression, use a def
|
||||
# W504: line break after binary operator
|
||||
pycodestyle src/flask_debugtoolbar test --ignore=E731,W504
|
||||
[testenv:style]
|
||||
deps = pre-commit
|
||||
skip_install = true
|
||||
commands = pre-commit run --all-files
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length = 100
|
||||
[testenv:typing]
|
||||
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