236 Commits
0.9.0 ... main

Author SHA1 Message Date
David Lord
1e22ee143c remove slsa provenance (#300) 2025-06-14 13:13:45 -07:00
David Lord
38a7511f87 remove slsa provenance
PyPI and trusted publishing has built-in attestation support now.
2025-06-14 13:09:14 -07:00
Kartikey Porwal
40f8645ec9 Fix class attribute dump_filename in ProfilerDebugPanel (#294)
We need `dump_filename` in `ProfilerDebugPanel` always initialised to avoid `AttributeError`

Fix #286
2025-01-06 11:15:32 -08:00
Jeff Widman
07f85152b5 Jquery update (#290)
Bump tablesorter version to 2.3.0

Tablesorter only requires jquery 1.2.6+ which is long deprecated.
I've updated deprecated functions in Tablesorter and raised the required version to jQuery 3.5+

Updated deprecated jquery functions in toolbar.js as well. I'm sure there are more but I've cleaned up the obvious ones that seem to be throwing most console warnings.
2024-11-06 15:27:49 -07:00
Max Garin
cbac0064df Remove deprecated bind, unbind, and $.trim 2024-10-18 10:22:47 -05:00
Max Garin
58b4dd0290 Update tablesorter version. Remove deprecated bind, unbind, and trim. Bump jquery required version. 2024-10-18 10:20:36 -05:00
David Lord
e3ce6eb0a6 Release version 0.16.0 (#282) 2024-09-28 07:55:51 -07:00
Adam Englander
98c611ade9 Release version 0.16.0 2024-09-28 07:47:33 -07:00
David Lord
95b02b5920 Bump the python-requirements group across 1 directory with 4 updates (#287) 2024-09-28 07:38:30 -07:00
David Lord
bd642464f2 [pre-commit.ci] pre-commit autoupdate (#283) 2024-09-28 07:38:00 -07:00
dependabot[bot]
5671a92e17 Bump the python-requirements group across 1 directory with 4 updates
Bumps the python-requirements group with 4 updates in the /requirements directory: [build](https://github.com/pypa/build), [pytest](https://github.com/pytest-dev/pytest), [mypy](https://github.com/python/mypy) and [pyright](https://github.com/RobertCraigie/pyright-python).


Updates `build` from 1.2.1 to 1.2.2
- [Release notes](https://github.com/pypa/build/releases)
- [Changelog](https://github.com/pypa/build/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/build/compare/1.2.1...1.2.2)

Updates `pytest` from 8.3.2 to 8.3.3
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.3)

Updates `mypy` from 1.11.1 to 1.11.2
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11.1...v1.11.2)

Updates `pyright` from 1.1.374 to 1.1.382.post1
- [Release notes](https://github.com/RobertCraigie/pyright-python/releases)
- [Commits](https://github.com/RobertCraigie/pyright-python/compare/v1.1.374...v1.1.382.post1)

---
updated-dependencies:
- dependency-name: build
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: pyright
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-27 15:10:13 +00:00
dependabot[bot]
ad6323994f Bump the github-actions group with 3 updates (#285)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 08:04:50 -07:00
pre-commit-ci[bot]
2361256107 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.5.0 → v0.6.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.0...v0.6.3)
2024-09-02 20:56:19 +00:00
Mac Newbold
e7b8136dea Bump the github-actions group with 3 updates (#280) 2024-08-02 16:28:46 -06:00
Mac Newbold
ccf5ae22c6 Merge branch 'main' into dependabot/github_actions/github-actions-5070efb06b 2024-08-02 16:27:41 -06:00
Mac Newbold
4e98b183f3 Bump the python-requirements group in /requirements with 2 updates (#279) 2024-08-02 16:27:25 -06:00
dependabot[bot]
5b4f4a0fcd Bump the github-actions group with 3 updates
Bumps the github-actions group with 3 updates: [actions/setup-python](https://github.com/actions/setup-python), [actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact).


Updates `actions/setup-python` from 5.1.0 to 5.1.1
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](82c7e631bb...39cd14951b)

Updates `actions/upload-artifact` from 4.3.3 to 4.3.4
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](65462800fd...0b2256b8c0)

Updates `actions/download-artifact` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](65a9edc588...fa0a91b85d)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 08:57:47 +00:00
dependabot[bot]
6f3eae808f Bump the python-requirements group in /requirements with 2 updates
Bumps the python-requirements group in /requirements with 2 updates: [mypy](https://github.com/python/mypy) and [pyright](https://github.com/RobertCraigie/pyright-python).


Updates `mypy` from 1.11.0 to 1.11.1
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.11...v1.11.1)

Updates `pyright` from 1.1.373 to 1.1.374
- [Release notes](https://github.com/RobertCraigie/pyright-python/releases)
- [Commits](https://github.com/RobertCraigie/pyright-python/compare/v1.1.373...v1.1.374)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: pyright
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-01 08:13:36 +00:00
Adam Englander
ffcb3f58df Bump the github-actions group with 2 updates (#271) 2024-07-28 20:42:34 -04:00
Adam Englander
b673c8d82a Merge branch 'main' into dependabot/github_actions/github-actions-15fc6d9b70 2024-07-28 20:40:22 -04:00
Adam Englander
63308c7f49 Bump the python-requirements group across 1 directory with 4 updates (#278) 2024-07-28 20:39:49 -04:00
Adam Englander
b07b074223 Merge branch 'main' into dependabot/pip/requirements/python-requirements-b4fa06ea3a 2024-07-28 20:38:20 -04:00
Adam Englander
e005409ac4 [pre-commit.ci] pre-commit autoupdate (#273) 2024-07-28 20:35:29 -04:00
Adam Englander
969bc454b6 Merge branch 'main' into pre-commit-ci-update-config 2024-07-28 20:34:15 -04:00
dependabot[bot]
e6ac868826 Bump the python-requirements group across 1 directory with 4 updates
Bumps the python-requirements group with 4 updates in the /requirements directory: [mypy](https://github.com/python/mypy), [pyright](https://github.com/RobertCraigie/pyright-python), [pytest](https://github.com/pytest-dev/pytest) and [tox](https://github.com/tox-dev/tox).


Updates `mypy` from 1.10.0 to 1.11.0
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.10.0...v1.11)

Updates `pyright` from 1.1.365 to 1.1.373
- [Release notes](https://github.com/RobertCraigie/pyright-python/releases)
- [Commits](https://github.com/RobertCraigie/pyright-python/compare/v1.1.365...v1.1.373)

Updates `pytest` from 8.2.1 to 8.3.2
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.2.1...8.3.2)

Updates `tox` from 4.15.0 to 4.15.1
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.15.0...4.15.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-requirements
- dependency-name: pyright
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-requirements
- dependency-name: tox
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: python-requirements
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-29 00:33:48 +00:00
Adam Englander
76a51dccf6 Bump the python-requirements group across 1 directory with 4 updates (#275) 2024-07-28 20:32:37 -04:00
Adam Englander
a1b9054479 Merge branch 'main' into dependabot/pip/requirements/python-requirements-1a0b311413 2024-07-28 20:30:52 -04:00
Adam Englander
2ea3823ae5 Bump certifi from 2024.2.2 to 2024.7.4 in /requirements (#276) 2024-07-28 20:30:26 -04:00
Adam Englander
02d6beff23 Merge branch 'main' into dependabot/pip/requirements/certifi-2024.7.4 2024-07-28 20:29:22 -04:00
Adam Englander
2f07e43da0 Bump zipp from 3.18.1 to 3.19.1 in /requirements (#277) 2024-07-28 20:28:58 -04:00
Adam Englander
654e80c494 Merge branch 'main' into dependabot/pip/requirements/zipp-3.19.1 2024-07-28 20:27:52 -04:00
Mac Newbold
877e69dc94 Add support for Flask.host_matching (#274) 2024-07-15 14:24:43 -06:00
Mac Newbold
44ee4b5e3a Merge branch 'main' into add-host-support-v2 2024-07-15 14:23:50 -06:00
dependabot[bot]
05104beefc Bump zipp from 3.18.1 to 3.19.1 in /requirements
Bumps [zipp](https://github.com/jaraco/zipp) from 3.18.1 to 3.19.1.
- [Release notes](https://github.com/jaraco/zipp/releases)
- [Changelog](https://github.com/jaraco/zipp/blob/main/NEWS.rst)
- [Commits](https://github.com/jaraco/zipp/compare/v3.18.1...v3.19.1)

---
updated-dependencies:
- dependency-name: zipp
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-09 19:24:17 +00:00
dependabot[bot]
c3c3d5ec98 Bump certifi from 2024.2.2 to 2024.7.4 in /requirements
Bumps [certifi](https://github.com/certifi/python-certifi) from 2024.2.2 to 2024.7.4.
- [Commits](https://github.com/certifi/python-certifi/compare/2024.02.02...2024.07.04)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-06 01:57:29 +00:00
dependabot[bot]
a2362ec4dd Bump the python-requirements group across 1 directory with 4 updates
Bumps the python-requirements group with 4 updates in the /requirements directory: [mypy](https://github.com/python/mypy), [pyright](https://github.com/RobertCraigie/pyright-python), [pytest](https://github.com/pytest-dev/pytest) and [tox](https://github.com/tox-dev/tox).


Updates `mypy` from 1.10.0 to 1.10.1
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.10.0...v1.10.1)

Updates `pyright` from 1.1.365 to 1.1.370
- [Release notes](https://github.com/RobertCraigie/pyright-python/releases)
- [Commits](https://github.com/RobertCraigie/pyright-python/compare/v1.1.365...v1.1.370)

Updates `pytest` from 8.2.1 to 8.2.2
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.2.1...8.2.2)

Updates `tox` from 4.15.0 to 4.15.1
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.15.0...4.15.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: pyright
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: tox
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: python-requirements
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-04 21:37:04 +00:00
dependabot[bot]
449405e3ba Bump urllib3 from 2.2.1 to 2.2.2 in /requirements (#270)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-04 14:32:42 -07:00
Samuel Williams
9b0b63465a Add support for Flask.host_matching
Allows configuration of Flask-DebugToolbar to support a Flask app
running in `host_matching` mode.

When Flask is configured this way, routes are each tied to a `host`
definition. This can either be a single explicit host, or a host
definition that includes variable values similar to Werkzeug path
definitions, eg `<subdomain>.toolbar.com`.

Handling explicit domains is simple, as the host can be passed through
directly - and this happens automatically.

If the host contains any variable parts, then calls to `url_for` need to
be able to access the appropriate values for those variables. If the
host string specified by the user contains arbitrary variables, it's
difficult for the toolbar to know what those should evaluate to. So we
restrict the possible options for the toolbar host to one of two options
here: either a single explicit host, or a full-wildcard host. The
wildcard host is managed internally by Flask-DebugToolbar so that we
know: 1) the variable name, and 2) what value to inject for it (the
current request's host).
2024-07-02 08:42:27 +01:00
pre-commit-ci[bot]
6848e5440b [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.7 → v0.5.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.7...v0.5.0)
2024-07-01 21:56:01 +00:00
dependabot[bot]
c0539265ca Bump the github-actions group with 2 updates
Bumps the github-actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish).


Updates `actions/checkout` from 4.1.6 to 4.1.7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](a5ac7e51b4...692973e3d9)

Updates `pypa/gh-action-pypi-publish` from 1.8.14 to 1.9.0
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](81e9d935c8...ec4db0b4dd)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 08:14:07 +00:00
David Lord
b368ff9004 Bump requests from 2.31.0 to 2.32.0 in /requirements (#264) 2024-06-08 14:26:40 -07:00
dependabot[bot]
ad9f1c0783 ---
updated-dependencies:
- dependency-name: requests
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-08 14:25:52 -07:00
David Lord
314ef64e2e Bump jinja2 from 3.1.3 to 3.1.4 in /requirements (#263) 2024-06-08 14:24:08 -07:00
dependabot[bot]
d4e1b1856b Bump jinja2 from 3.1.3 to 3.1.4 in /requirements
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-08 14:23:21 -07:00
David Lord
0fbc6210a8 [pre-commit.ci] pre-commit autoupdate (#262) 2024-06-08 14:22:45 -07:00
pre-commit-ci[bot]
7d3a6e3733 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.4.2 → v0.4.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.4.2...v0.4.7)
2024-06-08 14:21:16 -07:00
David Lord
1f904cfa8c Bump actions/checkout from 4.1.4 to 4.1.6 in the github-actions group (#266) 2024-06-08 14:20:54 -07:00
dependabot[bot]
6174ae7539 Bump actions/checkout from 4.1.4 to 4.1.6 in the github-actions group
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4.1.4 to 4.1.6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](0ad4b8fada...a5ac7e51b4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-08 14:20:00 -07:00
David Lord
193a3ed4f2 Bump the python-requirements group in /requirements with 4 updates (#265) 2024-06-08 14:19:34 -07:00
dependabot[bot]
1c82c2861f Bump the python-requirements group in /requirements with 4 updates
Bumps the python-requirements group in /requirements with 4 updates: [pyright](https://github.com/RobertCraigie/pyright-python), [pytest](https://github.com/pytest-dev/pytest), [types-pygments](https://github.com/python/typeshed) and [pygments](https://github.com/pygments/pygments).


Updates `pyright` from 1.1.360 to 1.1.365
- [Release notes](https://github.com/RobertCraigie/pyright-python/releases)
- [Commits](https://github.com/RobertCraigie/pyright-python/compare/v1.1.360...v1.1.365)

Updates `pytest` from 8.2.0 to 8.2.1
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.2.0...8.2.1)

Updates `types-pygments` from 2.17.0.20240310 to 2.18.0.20240506
- [Commits](https://github.com/python/typeshed/commits)

Updates `pygments` from 2.17.2 to 2.18.0
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.17.2...2.18.0)

---
updated-dependencies:
- dependency-name: pyright
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-requirements
- dependency-name: types-pygments
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-requirements
- dependency-name: pygments
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-requirements
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-01 08:27:42 +00:00
David Lord
e529ad95bc Bump werkzeug from 3.0.2 to 3.0.3 in /requirements (#261) 2024-05-06 18:56:10 -07:00
dependabot[bot]
f4702f45fb Bump werkzeug from 3.0.2 to 3.0.3 in /requirements
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-06 19:51:48 +00:00
David Lord
a2a2b1382c remove __version__ (#260) 2024-04-30 15:10:08 -07:00
David Lord
a0ccd43e8f use feature detection for jinja extensions 2024-04-30 15:09:09 -07:00
David Lord
71b7f6f0ce remove __version__ 2024-04-30 15:04:59 -07:00
David Lord
cf69fb7f1e deprecate __version__ 2024-04-30 11:35:01 -07:00
Mac Newbold
1d990e7f52 add static type annotations (#259) 2024-04-30 12:14:19 -06:00
David Lord
cdcf917044 add static type annotations 2024-04-30 10:55:06 -07:00
Mac Newbold
691acc186b apply formatting and linting rules (#258) 2024-04-30 11:53:37 -06:00
David Lord
3573841522 apply formatting and linting rules 2024-04-30 08:28:03 -07:00
David Lord
879918429e fix links 2024-04-30 07:36:57 -07:00
David Lord
4ccb3c9be6 update project files (#257) 2024-04-29 15:38:40 -07:00
David Lord
128bd5af6f update project files
use standard project layout and tool config across all pallets and
pallets-eco projects

* add issue templates
* add dependabot grouped updates for actions and pypi
* add lock inactive closed issues workflow
* add publish workflow with slsa and trusted publishing
* simplify tests workflow matrix
* simplify docs config.py
* show license in docs
* use pip-compile to pin development dependencies
* rename test to tests
* add .editorconfig
* simplify .gitignore
* add pre-commit hooks (will run formatters  in a subsequent PR)
* pin os and python in .readthedocs.yaml
* update license with original commit date and pallets-eco
* use pyproject.toml and flit_core instead of setuptools
* only declare flask dependency
* add config for mypy and pyright (will add typing in a subsequent PR)
* readme is markdown
* add pallets-eco message to readme
* remove install and docs links from readme
* add style, typing, docs tox envs
* use faster wheel building tox config
* add tox command to update dev dependencies
2024-04-29 10:54:08 -07:00
David Lord
dfb101fb18 Bump Flask to >=2.3.0 (#256) 2024-04-29 10:39:12 -07:00
Jeff Widman
9c9f24a2f2 Bump Flask to >=2.3.0
Now that we've dropped support for Python `3.7`,
it probably makes sense to also bump our minimum
version of Flask to `2.3.0`, as that is when Flask
dropped support for Python `3.7`: https://flask.palletsprojects.com/en/3.0.x/changes/#version-2-3-0
2024-04-29 17:31:49 +00:00
David Lord
bfd4d8506f Drop unnecessary Blinker dependency (#248) 2024-04-29 10:28:52 -07:00
Jeff Widman
677bf794aa Drop unnecessary Blinker dependency
Flask requires this, so it's a transitive dep, but it's not directly used within this toolbar at all.

We do directly import the other listed packages, so they should stay.
2024-04-29 11:20:24 -06:00
David Lord
c218bb1084 drop python 3.7 support (#255) 2024-04-29 09:55:17 -07:00
David Lord
ff3ed47d12 drop python 3.7 support 2024-04-29 09:13:37 -07:00
Jeff Widman
4225f7f4ad Bump to 0.15.1 (#254)
Bump to `0.15.1` release so that we can get this minor bugfix live:
* #253
2024-04-27 11:03:27 -07:00
Joel Burton
a63f64051b Fix prob w/using w/o SQLAlchemy (#253) 2024-04-25 14:47:07 -07:00
Mac Newbold
bf21647c4b Bump to 0.15.0 (#251) 2024-04-24 14:38:33 -06:00
Jeff Widman
818e5f2d62 Bump to 0.15.0
We've merged a number of deprecation fixes, let's get them live to our users via a new release.
2024-04-24 07:59:07 -07:00
Jeff Widman
c65c6b94e5 Add .readthedocs.yaml file (#246)
Add `.readthedocs.yaml` file.

This config file is now required by RTD in order to build the docs.
2024-04-24 14:46:37 +00:00
Jeff Widman
0ca1f6b241 Install custom Sphinx theme via extras_require (#250)
Previously the custom theme was vendored in via a `git` submodule...

Now this theme is available via a python package, so install it using `extras_require` instead.

This also unlocks letting other tooling such as ReadTheDocs install the custom theme without having to do custom git incantations.

I also removed the `index_logo` config as Sphinx warns that it's not a supported option by the theme. I grep'd the theme package, and it makes no mention of this option, besides we were leaving it blank already, so there's no point in having it.
2024-04-24 07:40:27 -07:00
Mac Newbold
48de4e4a3c Prevent LookupError when accessing debug_toolbars_var (#245) 2024-04-18 10:59:27 -06:00
Mac Newbold
b139c60a60 Merge branch 'master' into master 2024-04-18 10:58:04 -06:00
Marc Aymerich Gunern
05f23436ac Prevent LookupError when accessing debug_toolbars_var when toolbar is being disabled in the middle of a request 2024-04-18 12:16:43 +01:00
Jeff Widman
7012193220 Replace deprecated pkg_resources with stdlib (#239)
`pkg_resources` has been deprecated by `setuptools` for quite a while: https://setuptools.pypa.io/en/latest/pkg_resources.html

It's got some bugs/warts:

* https://github.com/pypa/setuptools/issues/2531
* https://discuss.python.org/t/will-setuptools-remove-pkg-resource-module-in-the-future/27182

So switch to using `importlib` functions which are part of the Python standard library as of `3.8`.

This is less error-prone, and also removes the need for `setuptools` to be installed in order for this panel to work.

I realize we technically still support `3.7`, but I thought it was fine to change this particular panel to require `3.8`, as `3.7` support is best effort given that it's now EOL'd by the core Python team.

I also removed the relative path location for specific libraries as it was simply blank for me on Python 3.12... I think showing the location of the site packages directory should suffice. If someone later wants to build this out further, they're more than welcome to.

Note that `importlib.metadata.distributions()` does have an outstanding issue that it reports a local editable install twice, but they plan to eventually fix that:
* https://github.com/pypa/setuptools/issues/4170
2024-04-13 13:54:46 +00:00
Jeff Widman
7f3defff7c Fix some jquery deprecation warnings (#242)
Ths is a follow-on PR to
https://github.com/pallets-eco/flask-debugtoolbar/pull/241 in order to fix the deprecation warnings:
* `bind()` replaced by `on()`
* `click(handler)` replaced by `on('click', handler)`

Three deprecation warnings remain, but they are caused by the `jquery.tablesorter.js` plugin, so need to be handled upstream:
* https://github.com/Mottie/tablesorter/issues/1787
* https://github.com/Mottie/tablesorter/pull/1786

Related:
* https://github.com/pallets-eco/flask-debugtoolbar/issues/166
2024-04-13 13:48:56 +00:00
Jeff Widman
969cce532d Bump jquery to 3.7.1 and jquery-migrate to 3.4.1 (#241)
Bump to the latest versions of `jquery` and `jquery-migrate`.

I popped open the browser console and I see five deprecation warnings, but 3 out of the 5 are triggered by `jquery.tablesorter.js` which we vendor, so nothing we can do there.

The other two we should address, but we can do so in a follow-on PR as they are merely deprecations at this point, not fatal errors.

So getting onto the `3.x` series, even with deprecation warnings, is a nice step forward.

Previously we shipped the minified version of `jquery`, and we could
continue to do so, but given this is a dev tool and we still have
warnings we need to fix, I opted for the non-minified version for now.

Additionally, they offer a `slim` varient that is stripped down... we
could probably use that too, but I didn't take the time to investigate.

Fix: https://github.com/pallets-eco/flask-debugtoolbar/issues/166
2024-04-13 13:42:23 +00:00
Jeff Widman
c2804c4917 Bump jquery to 1.12.4 (#236)
This will allow us to migrate to the latest version of jquery 1.x series.

After we're on 1.9.x we can upgrade to the 3.x series.

I bumped this, and then checked the developer console while clicking
through the various panels offered in FDBT... Everything worked as
expected and I got zero warnings from `jquery-migrate`. So I think it's
safe to pull this in.

This is a pre-requesite to upgrading to the latest version of `jquery`,
as per their docs we have to first bump to `1.9+`, then bump the version
of `jquery-migrate`, then can bump to latest `jquery` version.

While I could do this in a single PR, I thought simpler to do it as a
series of PRs for easier reversion if we later run into problems.

Related:
* https://github.com/pallets-eco/flask-debugtoolbar/issues/166
2024-04-13 13:39:41 +00:00
Jeff Widman
56beb35c36 Update jquery.tablesorter.js plugin (#235)
Update to the latest `v2.31.3` version of this `jquery` plugin, which
unblocks upgrading to `jquery` >= `1.9.x`.

Using a fork because:

1. The original repo hasn't been updated in seven years: https://github.com/christianbach/tablesorter
2. The most recent committer to the original repo ☝️  now maintains the fork.
3. The fork was updated just a few months ago.
4. The fork supports the latest versions of `jquery`:
     > Works with jQuery 1.9+ (`$.browser.msie` was removed; needed in the original version)

As far as I can tell, no other changes are needed.

This is a straight copy/paste of the non-minized code from: https://github.com/Mottie/tablesorter

I used non-minimized for simplicity, since this is a dev-only tool, the
perf impacts of minimization don't really matter.

I tested this manually by:

```shell
$ cd flask-debugtoolbar/test
$ flask --app basic_app run --debug
```

Then loading `http://127.0.0.1:5000/` in a recent version of Chrome and
finding a FDBT panel that uses the sorter. In this case, the SQLAlchemy
panel. I visually compared behavior pre/past upgrade and it appeared
identical.

Related:
* https://github.com/pallets-eco/flask-debugtoolbar/issues/166
2024-04-13 06:37:37 -07:00
Jeff Widman
98bf17aa58 Fix trailing whitespace (#237)
I noticed some trailing whitespace has crept into these files. It wasn't caught by `pycodestyle` because that only checks Python files.

Ideally we'd add a linter that catches these, but I didn't want to take the time to configure one right now, I just want my editor to stop flagging these while I'm working on trying to upgrade `jquery` versions.

Long term, we'll probably want a meta-linter like `prettier` or whatever the newest kid on the block is.
2024-04-12 16:27:04 +00:00
Grey Li
716f05d953 Remove deprecated Flask version attribute (#243)
The `flask.__version__` attr is deprecated and will be removed in the Flask 3.1 version. This PR replaced this attr to `importlib.metadata.version`.

We could remove the use of `pkg_resources` when we drop the Python 3.7 support.
2024-04-12 09:20:53 -07:00
Jeff Widman
fb28aa9d61 Fix missing distutils error on Python 3.12 (#238)
Running on Python 3.12 I see:

```shell
[2024-04-11 11:01:12,915] WARNING in toolbar: Disabled flask_debugtoolbar.panels.versions.VersionDebugPanel due to ImportError: import_string() failed for 'flask_debugtoolbar.panels.versions.VersionDebugPanel'. Possible reasons are:

- missing __init__.py in a package;
- package or module path not included in sys.path;
- duplicated package or module name taking precedence in sys.path;
- missing module, class, function or variable;

Debugged import:

- 'flask_debugtoolbar' found in '/Users/jeffwidman/Code/open-source/flask-debugtoolbar/src/flask_debugtoolbar/__init__.py'.
- 'flask_debugtoolbar.panels' found in '/Users/jeffwidman/Code/open-source/flask-debugtoolbar/src/flask_debugtoolbar/panels/__init__.py'.
- 'flask_debugtoolbar.panels.versions' not found.

Original exception:

ModuleNotFoundError: No module named 'distutils'
```

This is because Python 3.12 removed `distutils`.

Fix pulled from https://bugs.python.org/issue41282#msg393018.

This will not work on Python `3.6`, but we've already dropped support for that.

After the fix, the error is gone and the Versions panel re-appears.
2024-04-11 18:18:04 -07:00
Mac Newbold
efe447fb5f Fix SQLAlchemy recording warning for versions >= 3 (#233) 2024-03-20 09:28:47 -06:00
Jonty Wareing
9656a2cf33 Fix SQLAlchemy recording warning for versions >= 3
Since SQLAlchemy 3.0 `SQLALCHEMY_RECORD_QUERIES` is no longer
automatically enabled when Flask is running in debug or testing mode so
the debug toolbar warning will never be shown.

https://flask-sqlalchemy.palletsprojects.com/en/3.0.x/config/#flask_sqlalchemy.config.SQLALCHEMY_RECORD_QUERIES
2024-03-20 14:25:36 +00:00
Mac Newbold
9b63ad1837 Support applications using flask.copy_current_request_context (#231) 2024-01-17 09:40:50 -07:00
Marc Aymerich Gunern
765f22126e Handle cases when debug toolbar has not been installed 2024-01-11 18:06:28 +00:00
Marc Aymerich Gunern
d4a8cc963e Support applications using flask.copy_current_request_context 2024-01-09 13:50:02 +00:00
Mac Newbold
b7f5a725cd Add init_app() method to DebugPanel base class (#95) 2023-12-20 09:35:17 -07:00
Malthe Jørgensen
5bf5e093bb [Add] init_app()-method to DebugPanel base class
Adds a method `DebugPanel.init_app()` that gets called from
`DebugToolbar.init_app()`.

This allows DebugPanels to register their own routes, and do setup work
that should go across requests (as opposed to per-request setup) and
needs access to the Flask `app`-object.
2023-12-15 10:14:33 +01:00
Mac Newbold
719fe02df5 Add option to dump profiler stats (#204) 2023-12-13 10:49:10 -07:00
Dosenpfand
f18bcc708a Remove trailing whitespaces 2023-12-13 10:47:24 -07:00
Dosenpfand
95c2b86bcd Add profile dump stats option 2023-12-13 10:47:24 -07:00
Mac Newbold
b03a2e6fb3 Require Flask >= 2.2.0 (#224) 2023-12-11 11:27:45 -07:00
Jeff Widman
1c39a9ce47 Require Flask >= 2.2.0, Python >= 3.7
Require Flask >= `2.2.0`.

I'm comfortable going up to requiring `3.x`, but when I grep'd for
places we use older Flask constructs, this was all I found.

So for now no need to jump further.

Flask `2.2.0` requires Python >= `3.7`, so also dropped older pythons.
2023-12-11 11:25:52 -07:00
Mac Newbold
8c8b2bb35c Add minimal test without extra deps (#230) 2023-12-10 20:18:10 -07:00
Grey Li
a2e773124f Add minimal test without extra deps
To prevent issues like https://github.com/pallets-eco/flask-debugtoolbar/pull/225,
This PR add a minimal test that no extra deps are involved, it only install
the package, then try to import the extension class.

fixes #226
2023-12-10 12:54:29 +08:00
Jeff Widman
2f8ec9027b Bump to version 0.14.1 (#227) 2023-12-07 09:31:28 -08:00
Grey Li
ab9a41df6a Add the missing install requirement packaging (#225)
The `packaging` lib is not listed in the `install_requires` list.

https://github.com/greyli/flask-extension-status/actions/runs/7129071225/job/19412328108

```
Run python -c "from flask import Flask; app = Flask(__name__); from flask_debugtoolbar import DebugToolbarExtension; DebugToolbarExtension(app)"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/opt/hostedtoolcache/Python/3.12.0/x64/lib/python3.12/site-packages/flask_debugtoolbar/__init__.py", line 6, in <module>
    from packaging import version as version_builder
ModuleNotFoundError: No module named 'packaging'
Error: Process completed with exit code 1.
```
2023-12-07 08:32:29 -08:00
Jeff Widman
2b1e7d9907 Bump version to 0.14.0 (#223)
We need this to land before we can push a release...
2023-12-06 16:48:19 -08:00
Mac Newbold
d0360218fd Drop CHANGES.rst in favor of GitHub Releases (#198) 2023-11-21 08:26:28 -07:00
Mac Newbold
e9fd3072a9 Merge branch 'master' into switch-to-using-github-auto-generated-releases 2023-11-21 08:24:53 -07:00
Grey Li
1aedfb0e2e Use standard Python gitignore file (#220)
Created from https://github.com/github/gitignore/blob/main/Python.gitignore
2023-11-20 18:29:03 -08:00
Grey Li
e6ae9d0288 Fix the test for basic app (#221)
1. Update the test app

If I understand correctly, the debug toolbar will only be enabled if `app.debug` is `True`. So I added the `DEBUG=True` to the test app.

Related code: 2b8bf9cc44/src/flask_debugtoolbar/__init__.py (L114)

2. Update the `src/flask_debugtoolbar/__init__.py`

Fix the two if statements to prevent the following errors:

```
>       if 'gzip' in response.headers.get('Content-Encoding'):
E       TypeError: argument of type 'NoneType' is not iterable
```

Since the `response.headers.get('Content-Encoding')` could be None.

With this PR, all the tests will be passed. The failed style checker will be fixed in #219
2023-11-20 18:21:38 -08:00
Grey Li
62ce443f8b Fix lint issues and lint config (#219)
For the two ignored rules:

- E731: It's OK to use lambda
- W504: W503 and W504 are conflicts with each other, we need to disable one of them.
2023-11-20 10:59:24 -08:00
Grey Li
2b8bf9cc44 Remove the use of before_first_request (#218) 2023-11-17 22:12:44 +08:00
Grey Li
42d859534a Remove the use of before_first_request 2023-11-17 22:09:25 +08:00
Grey Li
f959951185 Fix tox and GitHub actions settings (#217)
* Remove the branch constraint for pull request triggering
* Use Python 3.12 for main tests
* Use `ubuntu-20.04` for Python 3.6 since it's been removed in `ubuntu-latest`
* Remove Python 2.7 since it's not supported by GitHub Actions anymore (https://github.com/actions/setup-python/issues/672)
* Add the missing `setup-python` step
* Merge the `pip install` commands
2023-11-16 14:41:30 -08:00
dadavec
e1c8704444 Remove deprecated charset property from process_response content crafting (#211)
The `Request.charset` property is deprecated since Werkzeug version 2.3. It was removed in Werkzeug 3.0. Request data must always be UTF-8.

https://werkzeug.palletsprojects.com/en/2.3.x/wrappers/#werkzeug.wrappers.Request.charset
2023-11-15 19:53:02 -08:00
Mac Newbold
8a4cfa5e3c No need to specify custom default value if key not found (#210) 2023-11-15 17:30:39 -07:00
Grey Li
51d105afad Set up GitHub actions to replace Travis (#215) 2023-11-16 02:10:00 +08:00
Grey Li
15192f19e0 Set up github actions 2023-11-15 22:28:43 +08:00
Jeff Widman
5712e57869 No need to specify custom default value if key not found
The `''` arg specifies a custom default value if the key isn't found. However, the default of `None` works fine for boolean testing:

```python
>>> 'gzip' in [None]
False
```

I missed this when I originally reviewed https://github.com/pallets-eco/flask-debugtoolbar/pull/154.
2023-10-13 13:39:35 +00:00
Hiromasa Ihara
9571d06df5 fix: drop response.charset because charset deprecated (#206)
I took a quick peek at upstream to see if this has any backwards-breaking issues whereby we'd need to check the `werkzeug` version, but from my reading it looks it should be fine most of the time.

Also, this was deprecated back in April's `2.3.0` release and then removed in the recent `3.0.0` release, so I suspect most of our userbase will have already migrated to those versions.

Given this lib is a dev-tooling library not typically used in production, I'm not too worried about ensuring we support the 0.01% case where someone is using an old version of `werkzeug` + a custom charset.

More context:
* https://github.com/pallets/werkzeug/issues/2602
* https://github.com/pallets/werkzeug/pull/2641
* https://github.com/pallets/werkzeug/pull/2768
2023-10-13 06:35:25 -07:00
Nick Janetakis
3b25e114e9 fix: use urllib.parse.quote_plus and drop werkzeug.urls.url_quote_plus (#207) 2023-10-06 18:19:51 -04:00
Hiromasa Ihara
f7ae3fd591 fix: use urllib.parse.quote_plus and drop werkzeug.urls.url_quote_plus 2023-10-01 16:03:15 +09:00
Hiromasa Ihara
ce02d2da3c fix: migrate from deprecated flask.Markup to markupsafe.Markup (#203) 2023-05-24 10:22:56 -07:00
Jeff Widman
bd346a0fc1 Drop CHANGES.rst in favor of GitHub Releases
I started to cut a new release but realized it's a bit painful that we
are currently maintaining both a `CHANGES.rst` file and also tagging
releases in GitHub releases UI.

For a busy project, maintaining a dedicated changelog makes sense... but
the reality is this project is in maintenance mode, and we the
maintainers are more likely to cut releases when they're easy/low
friction. Since GitHub very nicely has the "Auto-generate-release-notes"
button, let's just use that.

I considered copy/pasting the results from that to `CHANGES.rst`, but
even that feels like a waste of time.
2023-01-17 20:05:25 +00:00
Jeff Widman
ed8243e17e Point URLs at pallets-eco/flask-debugtoolbar (#197) 2023-01-17 08:16:30 -08:00
Nick Janetakis
02c99a7b64 Fix Flask SQLAlchemy quickstart link (#196) 2022-12-27 08:10:52 -05:00
Francesco Frassinelli
96514793e4 Update sqlalchemy_error.html 2022-12-27 13:10:16 +01:00
Francesco Frassinelli
ec8cc3a0ba Fix Flask SQLAlchemy quickstart link 2022-12-27 10:54:53 +01:00
Jeff Widman
e3c8ab0ca2 Point at new location of django-debug-toolbar (#189) 2022-11-02 12:53:49 -07:00
Jeff Widman
fefb32b04d Fix outdated docs links (#187) 2022-11-02 12:51:06 -07:00
Nick Janetakis
b5a7c032ab Merge pull request #186 from Dosenpfand/flask-sqlalchemy-v3
Flask-SQLAlchemy 3 compatibility
2022-10-28 11:41:38 -04:00
Dosenpfand
6af24f5f44 Do not explicitly set SQLALCHEMY_TRACK_MODIFICATIONS for test/example 2022-10-28 11:28:50 +02:00
Dosenpfand
b4a197f87f Enable query recording if debug is active, remove warning 2022-10-26 14:31:14 +02:00
Dosenpfand
15b6fee933 Fix typo 2022-10-24 21:29:59 +02:00
Dosenpfand
9e03576c94 Keep config for flask_sqlalchemy < 3 2022-10-24 19:35:47 +02:00
Dosenpfand
bfa48c5a2c Add support for flask_sqlalchemy >= 3.0.0 2022-10-24 18:59:28 +02:00
sur.la.route
890fd9c7fb updated to work with flask 2.2+ (#183)
Replaced `_request_ctx_stack.top` with `request_ctx` per https://github.com/pallets/flask/pull/4682

This checks the version of `flask` and only does the switcheroo on newer versions of flask...
2022-10-04 16:20:14 -07:00
Hugo van Kemenade
42a9651950 Replace deprecated threading.currentThread with threading.current_thread (#179) 2022-09-18 14:08:43 -07:00
Nick Janetakis
ff01910f6a Merge pull request #182 from caffeinatedMike/master
Fixed scrollbar issues
2022-08-18 11:20:45 -04:00
Michael Hill
9cdb04edcb Make flDebugToolbar vertically scrollable
When on small screens where the debug toolbar is longer than the screen there was no way to access the items listed at the bottom of the toolbar. Adding `overflow-y: auto` allows a scrollbar to be used when this is the case.
2022-08-02 09:41:44 -04:00
Michael Hill
f546d4633b Fixed classname and bottom padding for scrollbar 2022-07-26 19:08:25 -04:00
Michael Hill
db07028aa0 Update scroll classname to make base.html template 2022-07-26 19:07:03 -04:00
Nick Janetakis
d44ab40729 Merge pull request #180 from timgates42/bugfix_typos
docs: Fix a few typos
2022-07-13 17:03:28 -04:00
Tim Gates
0d409f54f5 docs: Fix a few typos
There are small typos in:
- src/flask_debugtoolbar/panels/profiler.py
- src/flask_debugtoolbar/static/codemirror/mode/rst/rst.js
- src/flask_debugtoolbar/static/codemirror/mode/xquery/xquery.js
- src/flask_debugtoolbar/static/js/jquery.tablesorter.js

Fixes:
- Should read `second` rather than `secound`.
- Should read `preceded` rather than `preceeded`.
- Should read `initial` rather than `inital`.
- Should read `divided` rather than `divded`.
- Should read `debugging` rather than `debuging`.
- Should read `convenience` rather than `conveinence`.
- Should read `capabilities` rather than `capabilitys`.
2022-07-14 06:34:03 +10:00
Nate Collins
708df1c07a Expand HTTP codes on which the toolbar will be displayed (#176)
Change to expand what HTTP codes the toolbar displays on. Currently it only displays on 200 (or redirects when DEBUG_TB_INTERCEPT_REDIRECTS is enabled). However, debugging or verifying non-200 pages is commonly necessary. This change would enable display of the toolbar on other pages that may serve HTML, specifically: 201, 400, 401, 403, 404, 405, 500, 501, 502, 503, 504
2022-04-01 13:32:48 -07:00
Nathan Collins
10186a4202 Permit scrolling for content panels 2022-04-01 13:30:42 -07:00
Nate Collins
3cd2dceace Add ARIA role to toolbar for accessibility improvement (#174)
Change to improve accessibility compliance, for example useful when a developer is using a screen reader. Using the ARIA role attribute is a HTML4 compatible way of accomplishing this.

For reference: https://dequeuniversity.com/rules/axe/4.3/region

Co-authored-by: Megan Schanz <schanzme@msu.edu>
2022-04-01 13:30:16 -07:00
Megan Schanz
376c3deab3 Submodule to use https protocol after unencrypted git proto deprecated
Ref: https://github.blog/2021-09-01-improving-git-protocol-security-github/
2022-04-01 13:29:24 -07:00
Nick Janetakis
45d3588bb6 Release 0.13.1 2022-03-29 18:39:20 -04:00
Nick Janetakis
15e8d77a49 Fix changelog formatting for 0.12.X 2022-03-29 18:10:40 -04:00
Nick Janetakis
4d84f262ae Merge pull request #172 from flask-debugtoolbar/fix-setupcfg
Move library into src/ directory
2022-03-29 18:08:01 -04:00
Nick Janetakis
db64ce632c Move library into src/ directory
After building this locally I noticed all of the expected files were in
the wheel where as before this patch it was missing a lot of files.

This idea of using a src/ directory is mentioned in the official Python
documentation for packaging files at:

https://packaging.python.org/en/latest/tutorials/packaging-projects/

It's also used in Flask and other large Python projects.
2022-03-29 08:18:21 -04:00
Jeff Widman
956d7501ec Bump version for development 2022-03-28 01:19:17 -07:00
Jeff Widman
a758a9df7a Release 0.12.1 2022-03-28 01:16:17 -07:00
Jeff Widman
5eea25882c Fix changelog / docs link
I fat-fingered this in
https://github.com/flask-debugtoolbar/flask-debugtoolbar/pull/164 so
correcting it.
2022-03-28 01:14:12 -07:00
Jeff Widman
e954cd9fae Bump version for development 2022-03-27 23:48:24 -07:00
Jeff Widman
03d79be02c Release 0.12.0 2022-03-27 23:48:08 -07:00
Jeff Widman
7f17d2ce57 Update PyPI metadata files: add setup.cfg etc (#164)
Update to the newer PyPI / python packaging metadata file structure.

A lot of this was cribbed from how [`Flask`](https://github.com/pallets/flask) itself
exposes its metadata.
2022-03-26 21:51:54 -07:00
Nick Janetakis
30fba11f36 Remove with_ extension for Jinja 3.0 (#157)
Jinja 3.0 throws a deprecation warning that this will be removed in 3.1
because it's built into Jinja now without needing an extension.

However since folks might want to use Jinja 2 for a while this supports
both versions by only using the extension with Jinja 2.
2022-03-25 21:26:26 -07:00
jnnkB
d474a6a689 prefixed css classes, fixes #152 (#153) 2020-08-14 11:39:15 -07:00
zaw007
83d398d9d5 Support gzip response (#154)
* add gzip compress and decompress

* support gzip response
2020-08-14 11:37:37 -07:00
Jeff Widman
3929742d9c Cleanup version handling slightly (#149)
Some improvements I saw over on
https://github.com/FactoryBoy/factory_boy/pull/676/files that looked
useful here as well.
2020-03-09 13:10:02 -07:00
Jeff Widman
70abd78e55 Setup DB properly
When I switched over to `flask run` entrypoint in b92391d177,
I forgot that the `if name==__main__` code no longer triggers.
So the SQLite in-memory database wasn't getting created for the example
app.

This moves the DB creation to a werkzeug/Flask hook that runs before the
first request to the app, so that the DB table is created when we query
it.

Also updated the test which worked fine previously, but this is more
idiomatic.
2020-03-09 10:01:59 -07:00
Jeff Widman
dbea74b626 Update README.rst 2020-03-09 09:46:05 -07:00
Jeff Widman
c6102aeb14 Change docs to pull version from setup.py 2020-03-02 09:37:37 -08:00
Jeff Widman
10c9c1ae5d Update Flask-SQLAlchemy links 2020-02-22 20:39:21 -08:00
Yaser Amiri
9e600c6e13 Add flask.g section to show g object content. 2020-02-22 09:47:21 -08:00
Matthew Swabey
9b8a8afa97 Fix SQLAlchemy SELECT/EXPLAIN to use url_for to respect app prefixes. Provide url_for to all toolbar templates 2020-02-22 09:24:23 -08:00
Jeff Widman
39ac97a7e0 pycodestyle fixes 2020-02-18 01:21:40 -08:00
Jeff Widman
a5cb5a709f Bump version for development 2020-02-18 01:19:49 -08:00
Jeff Widman
02064c76ed Release 0.11.0 2020-02-18 01:11:19 -08:00
Jeff Widman
d713732807 Cleanup tox/travis
* Switch to python 3.8 in Travis. I tried to add 3.8 while keeping 3.7
and 3.6, but ran into issues with Travis config, so instead just bumped
straight to 3.8. Long term I'd like to explore moving to Azure
Pipelines, but don't have the time to figure that out just yet.
* `flake8` was renamed to `pycodestyle`
* `py.test` was deprecated in favor of `pytest`
2020-02-18 00:42:39 -08:00
Jeff Widman
b92391d177 Switch to Flask's native CLI
Drop `flask_script` in favor of Flask's native CLI:
* https://flask.palletsprojects.com/en/master/cli/

This also requires changing the tests so that `pytest` mocks the env var
`FLASK_ENV` so that the test app starts in development mode. Unlike
normal test apps, we _do_ want development/debug mode, in addition to
testing mode.
2020-02-17 23:57:04 -08:00
Florian
4964ae261f RoutesList: Do not show debugtoolbar routes 2020-02-17 22:30:10 -08:00
Pierre GIRAUD
ad847299c4 Add doc for SQL syntax highlighting 2020-02-17 21:57:01 -08:00
Jeff Widman
7ce099c3d0 Remove deprecated request.is_xhr
This was removed from `werkzeug` `1.0.0`.

Details:
* https://github.com/pallets/werkzeug/issues/1077
* https://github.com/pallets/werkzeug/issues/1714 (search for `is_xhr`)

Fix #144.
2020-02-17 21:50:56 -08:00
Jeff Widman
9c7db48362 Explicitly disable SQLALCHEMY_TRACK_MODIFICATIONS
This silences deprecation warnings.
Background: https://stackoverflow.com/a/33790196/770425

Note: This code can be removed once `flask_sqlalchemy` 3.0 ships, or any
release that includes
https://github.com/pallets/flask-sqlalchemy/pull/727.
2020-02-17 21:36:01 -08:00
Tim Gates
88f15cba35 Fix simple typo: exapanded -> expanded
Closes #141
2020-02-06 14:03:36 -08:00
Matt Good
d852042ccb Merge pull request #119 from davidism/json-available
don't use flask.json_available
2018-02-07 09:52:11 -08:00
David Lord
5bd2e8a423 flask.json_available is a no-op
it is removed in Flask 1.0
2018-02-07 07:13:21 -08:00
Jeff Widman
c27256c00a Bump dev version 2017-02-12 03:33:05 -08:00
Jeff Widman
010206298e Release 0.10.1 2017-02-12 03:04:07 -08:00
Jeff Widman
678ec31229 Revert "Change docs to pull version from setup.py"
This reverts commit 45b70c952f.
This commit broke the readthedocs build because RTD is building the docs
without running setup.py. So there is no package metadata.
2017-02-12 02:48:44 -08:00
Jeff Widman
23d52703fd Make copyright string track current year 2017-02-12 02:33:47 -08:00
Jeff Widman
45b70c952f Change docs to pull version from setup.py 2017-02-12 02:32:47 -08:00
Jeff Widman
9f2c353e86 Trivial imports cleanup 2017-02-12 02:31:26 -08:00
Jeff Widman
cfe142b285 Point Flask-SQLAlchemy docs at ‘latest’ 2017-02-12 00:21:20 -08:00
Jeff Widman
19a25fd895 Add universal wheel support
Let’s roll!
2017-02-10 01:09:59 -08:00
Jeff Widman
8dcb97c05a Travis should use python 3.6 interpreter 2017-02-10 00:50:38 -08:00
Jeff Widman
b49af327cf Use containers for travis tests 2017-02-10 00:44:27 -08:00
Jeff Widman
38ab3a49c1 Test using modern python 2017-02-10 00:42:00 -08:00
Jeff Widman
9f901e34ae Remove deprecated tox arg —use-mirrors 2017-02-10 00:36:19 -08:00
Jeff Widman
f050a6b0de Use latest for flask-sqlalchemy docs rather than pinning version 2017-02-10 00:28:45 -08:00
Jeff Widman
e33f3e1c85 Update RTD links w/https & .io 2017-02-09 23:54:16 -08:00
Peter M. Landwehr
02ff95acde Add LICENSE file to MANIFEST.in / source bundle (#102) 2016-09-18 23:13:08 -07:00
Iuri de Silvio
2436239964 Additional cleanup of deprecated flask.ext.* magic imports. (#97) 2016-06-29 16:52:23 -07:00
Michael Lenzen
18a0030354 Fix deprecated import from flask.ext.sqlalchemy (#94) 2016-06-29 13:33:02 -07:00
Matt Good
ee726eece2 Better setup.py description 2015-04-17 17:53:40 -07:00
Matt Good
1cd9ba69d1 Update CHANGES for 0.10 2015-04-17 17:49:30 -07:00
Matt Good
30a1cd399a Add docs note on Versions panel displaying other packages 2015-04-17 17:49:30 -07:00
Matt Good
0524e7f3e9 Update "Thanks" section of docs 2015-04-17 17:49:30 -07:00
Matt Good
14cd912df7 Fix style errors and add to automated checks
Adds a "stylecheck" to the Tox build config to automatically run flake8 checks.
Fixes existing flake8 issues.
2015-04-17 14:23:58 -07:00
Matt Good
f087311f7c Add missing sqlalchemy_error template
Missed adding this file in the last commit.
2015-04-17 14:19:41 -07:00
Matt Good
61d1fc2678 Display steps needed to display SQL queries
In the SQLAlchemy panel, detect if Flask-SQLAlchemy is not set up to record
queries for the current app, and display the necessary steps to set it up.

The following steps will be detected and displayed if needed:

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

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

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

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

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

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

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

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

13
.editorconfig Normal file
View 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
View 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
View 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.

View 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
View 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
View 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
View 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
View 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
View 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

14
.gitignore vendored
View File

@@ -1,6 +1,10 @@
*.egg-info
*.pyc
build/
.idea/
.vscode/
.venv*/
venv*/
__pycache__/
dist/
docs/_build
.tox
.coverage*
htmlcov/
.tox/
docs/_build/

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "docs/_themes"]
path = docs/_themes
url = git://github.com/mitsuhiko/flask-sphinx-themes.git

21
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,21 @@
ci:
autoupdate_schedule: monthly
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.3
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-merge-conflict
exclude: "(codemirror|jquery)"
- id: debug-statements
exclude: "(codemirror|jquery)"
- id: fix-byte-order-marker
exclude: "(codemirror|jquery)"
- id: trailing-whitespace
exclude: "(codemirror|jquery)"
- id: end-of-file-fixer
exclude: "(codemirror|jquery)"

13
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,13 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: '3.12'
python:
install:
- requirements: requirements/docs.txt
- method: pip
path: .
sphinx:
builder: dirhtml
fail_on_warning: true

View File

@@ -1,9 +0,0 @@
language: python
python: "2.7"
env:
- TOXENV=py26
- TOXENV=py27
- TOXENV=py33
install:
- pip install tox --use-mirrors
script: tox

View File

@@ -1,111 +0,0 @@
Changes
=======
0.9.0 (2014-01-03)
--------
Enhancements:
- Python 3 compatibility (#54, thanks justinmayer and jmagnusson)
- Support .init_app() (#38)
- New "Config" panel displaying Flask config values (#51, thanks Alexey Diyan)
- Better PEP8-style formatting (#63, thanks Ivan Ivaschenko)
Fixes:
- Fix template editor with non-ASCII templates (#46)
0.8 (2013-02-21)
----------------
Enhancements:
- Use `itsdangerous <http://pythonhosted.org/itsdangerous/>`_ to sign SQL queries
- Expose the jQuery object as ``fldt.$`` so extensions can use the toolbar's
copy of jQuery (#42)
Fixes:
- Don't intercept redirects on XHR requests (#41)
- Fix SQL query time display as milliseconds (#36)
- Fix ``functools.partial`` error (#35)
- Fix werkzeug request logging with logging panel (#33)
- Fix SQL panel unicode encoding error (#31)
0.7.1 (2012-05-18)
------------------
Fixes:
- loading template editor in-place over current page
0.7 (2012-05-18)
----------------
Enhancements:
- Add an in-browser template editor to the template panel
- ``DEBUG_TB_PROFILER_ENABLED`` config option to enable the profiler on all
requests (normally it is user-enabled by clicking the checkmark)
0.6.3.1 (2012-04-16)
--------------------
New release to add missing changelog for 0.6.3
0.6.3 (2012-04-16)
------------------
Fixes:
- Compatibility with Flask-SQLAlchemy 0.16 package name
0.6.2 (2012-02-18)
------------------
Fixes:
- Installation issue on Windows with trailing slashes in MANIFEST.in
- JavaScript error when using conditional comments for ``<html>`` tag
(like in HTML5 Boilerplate)
0.6.1 (2012-02-15)
------------------
Fixes:
- Memory leak when toolbar was enabled
- UnicodeDecodeError when request data contained binary data (e.g. session values)
Enhancements:
- ``DEBUG_TB_ENABLED`` config setting to explicitly enable or disable the toolbar
- ``DEBUG_TB_HOSTS`` config setting to enable toolbar only for specific remote hosts
- New logo for Flask instead of Django
- Monospaced font on table data
Thanks to kennethreitz and joeshaw for their contributions.
0.6 (2012-01-04)
----------------
Flask 0.8 or higher is required
Enhancements:
- Flask 0.8 compatibility
Thanks to mvantellingen

View File

@@ -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

View File

@@ -1,2 +0,0 @@
recursive-include flask_debugtoolbar/templates *.html
recursive-include flask_debugtoolbar/static *

44
README.md Normal file
View 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
```
![](https://raw.githubusercontent.com/pallets-eco/flask-debugtoolbar/main/docs/_static/example.gif)

View File

@@ -1,43 +0,0 @@
Flask Debug-toolbar
===================
This is a port of the excellent `django-debug-toolbar <https://github.com/django-debug-toolbar/django-debug-toolbar>`_
for Flask applications.
.. image:: https://travis-ci.org/mgood/flask-debugtoolbar.png?branch=master
:target: https://travis-ci.org/mgood/flask-debugtoolbar
Installation
------------
Installing is simple with pip::
$ pip install flask-debugtoolbar
Usage
-----
Setting up the debug toolbar is simple::
from flask import Flask
from flask_debugtoolbar import DebugToolbarExtension
app = Flask(__name__)
# the toolbar is only enabled in debug mode:
app.debug = True
# set a 'SECRET_KEY' to enable the Flask session cookies
app.config['SECRET_KEY'] = '<replace with a secret key>'
toolbar = DebugToolbarExtension(app)
The toolbar will automatically be injected into Jinja templates when debug mode is on.
In production, setting ``app.debug = False`` will disable the toolbar.
See the `documentation`_ for more information.
.. _documentation: http://flask-debugtoolbar.readthedocs.org

View File

@@ -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

BIN
docs/_static/example.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

Submodule docs/_themes deleted from 0269f3d188

View File

@@ -1,246 +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 sys, os
# Project --------------------------------------------------------------
# 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('.'))
project = "Flask-DebugToolbar"
version = release = importlib.metadata.version("flask-debugtoolbar").partition(".dev")[
0
]
# -- General configuration -----------------------------------------------------
# General --------------------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.viewcode']
# 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, Matt Good'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.9'
# The full version, including alpha/beta/rc tags.
release = '0.9.0'
# 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 = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'flask_small'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
html_theme_options = {
'github_fork': 'mgood/flask-debugtoolbar',
'index_logo': None,
default_role = "code"
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.extlinks",
"sphinx.ext.intersphinx",
"sphinxcontrib.log_cabinet",
"pallets_sphinx_themes",
]
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 = {
"python": ("https://docs.python.org/3/", None),
"flasksqlalchemy": ("https://flask-sqlalchemy.palletsprojects.com", None),
}
# Add any paths that contain custom themes here, relative to this directory.
sys.path.append(os.path.abspath('_themes'))
html_theme_path = ['_themes']
# HTML -----------------------------------------------------------------
# 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

View File

@@ -1,21 +1,19 @@
.. Flask-DebugToolbar documentation master file, created by
sphinx-quickstart on Wed Feb 15 18:08:39 2012.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Flask-DebugToolbar
==================
This is a port of the excellent `django-debug-toolbar <https://github.com/django-debug-toolbar/django-debug-toolbar>`_
for Flask applications.
This extension adds a toolbar overlay to Flask applications containing useful information for debugging.
.. image:: _static/example.gif
Installation
------------
Installing is simple with pip::
Installing is simple with `pip`_::
$ pip install flask-debugtoolbar
.. _pip: https://pip.pypa.io/
Usage
-----
@@ -36,9 +34,16 @@ Setting up the debug toolbar is simple::
toolbar = DebugToolbarExtension(app)
The toolbar will automatically be injected into Jinja templates when debug mode is on.
In production, setting ``app.debug = False`` will disable the toolbar.
The toolbar will automatically be injected into HTML responses when debug mode
is on. In production, setting ``app.debug = False`` will disable the toolbar.
This extension also supports the Flask app factory pattern by separately
creating the toolbar and later initializing it for an app::
toolbar = DebugToolbarExtension()
# Then later on.
app = create_app('the-config.cfg')
toolbar.init_app(app)
Configuration
-------------
@@ -50,10 +55,19 @@ Name Description De
==================================== ===================================== ==========================
``DEBUG_TB_ENABLED`` Enable the toolbar? ``app.debug``
``DEBUG_TB_HOSTS`` Whitelist of hosts to display toolbar any host
``DEBUG_TB_ROUTES_HOST`` The host to associate with toolbar ``None``
routes (where its assets are served
from), or the sentinel value `*` to
serve from the same host as the
current request (ie any host). This
is only required if Flask is
configured to use `host_matching`.
``DEBUG_TB_INTERCEPT_REDIRECTS`` Should intercept redirects? ``True``
``DEBUG_TB_PANELS`` List of module/class names of panels enable all built-in panels
``DEBUG_TB_PROFILER_ENABLED`` Enable the profiler on all requests ``False``, user-enabled
``DEBUG_TB_TEMPLATE_EDITOR_ENABLED`` Enable the template editor ``False``
``DEBUG_TB_PROFILER_DUMP_FILENAME`` Filename of the profiler stats dump, ``None``, no dump will be written
can be a ``str`` or a ``callable``
==================================== ===================================== ==========================
To change one of the config options, set it in the Flask app's config like::
@@ -61,16 +75,24 @@ To change one of the config options, set it in the Flask app's config like::
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = False
Panels
------
.. toctree::
panels
license
Contributing
------------
Fork us `on GitHub <https://github.com/mgood/flask-debugtoolbar>`_
Fork us `on GitHub <https://github.com/pallets-eco/flask-debugtoolbar>`_
Thanks
------
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
This was based on the original `django-debug-toolbar`_. Thanks to `Michael van Tellingen`_ for the original development of this Flask extension, and to all the `individual contributors`_.
.. _django-debug-toolbar: https://github.com/jazzband/django-debug-toolbar
.. _Michael van Tellingen: https://github.com/mvantellingen
.. _individual contributors: https://github.com/pallets-eco/flask-debugtoolbar/graphs/contributors

5
docs/license.rst Normal file
View File

@@ -0,0 +1,5 @@
BSD-3-Clause License
====================
.. literalinclude:: ../LICENSE.txt
:language: text

110
docs/panels.rst Normal file
View File

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

50
example/app.py Normal file
View File

@@ -0,0 +1,50 @@
# Run using: `FLASK_DEBUG=True flask run`
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'] = (
# '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['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
toolbar = DebugToolbarExtension(app)
class ExampleModel(db.Model):
__tablename__ = "examples"
value = db.Column(db.String(100), primary_key=True)
@app.route("/")
def index():
app.logger.info("Hello there")
ExampleModel.query.get(1)
return render_template("index.html")
@app.route("/redirect")
def redirect_example():
response = redirect(url_for("index"))
response.set_cookie("test_cookie", "1")
return response
with app.app_context():
db.create_all()

View File

@@ -1,49 +0,0 @@
import sys
sys.path.insert(0, '.')
from flask import Flask, render_template, redirect, url_for
from flask.ext.script import Manager
from flask.ext.sqlalchemy import SQLAlchemy
from flask_debugtoolbar import DebugToolbarExtension
app = Flask(__name__)
app.config['DEBUG_TB_INTERCEPT_REDIRECTS'] = True
#app.config['DEBUG_TB_PANELS'] = (
# 'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
# 'flask_debugtoolbar.panels.logger.LoggingPanel',
# 'flask_debugtoolbar.panels.timer.TimerDebugPanel',
#)
#app.config['DEBUG_TB_HOSTS'] = ('127.0.0.1', '::1' )
app.config['SECRET_KEY'] = 'asd'
app.config['DEBUG'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
toolbar = DebugToolbarExtension(app)
class ExampleModel(db.Model):
__tablename__ = 'examples'
value = db.Column(db.String(100), primary_key=True)
@app.route('/')
def index():
app.logger.info("Hello there")
ExampleModel.query.get(1)
return render_template('index.html')
@app.route('/redirect')
def redirect_example():
response = redirect(url_for('index'))
response.set_cookie('test_cookie', '1')
return response
if __name__ == "__main__":
db.create_all()
manager = Manager(app)
manager.run()

View File

@@ -1,212 +0,0 @@
import os
from flask import current_app, request, g
from flask.globals import _request_ctx_stack
from flask import send_from_directory
from jinja2 import Environment, PackageLoader
from werkzeug.exceptions import HTTPException
from werkzeug.urls import url_quote_plus
from flask_debugtoolbar.toolbar import DebugToolbar
from flask_debugtoolbar.compat import iteritems
from flask import Blueprint
module = Blueprint('debugtoolbar', __name__)
def replace_insensitive(string, target, replacement):
"""Similar to string.replace() but is case insensitive
Code borrowed from:
http://forums.devshed.com/python-programming-11/case-insensitive-string-replace-490921.html
"""
no_case = string.lower()
index = no_case.rfind(target.lower())
if index >= 0:
return string[:index] + replacement + string[index + len(target):]
else: # no results so return the original string
return string
def _printable(value):
try:
return repr(value)
except Exception as e:
return '<repr(%s) raised %s: %s>' % (
object.__repr__(value), type(e).__name__, e)
class DebugToolbarExtension(object):
_static_dir = os.path.realpath(
os.path.join(os.path.dirname(__file__), 'static'))
_redirect_codes = [301, 302, 303, 304]
def __init__(self, app=None):
self.app = app
self.debug_toolbars = {}
# Configure jinja for the internal templates and add url rules
# for static data
self.jinja_env = Environment(
autoescape=True,
extensions=['jinja2.ext.i18n', 'jinja2.ext.with_'],
loader=PackageLoader(__name__, 'templates'))
self.jinja_env.filters['urlencode'] = url_quote_plus
self.jinja_env.filters['printable'] = _printable
if app is not None:
self.init_app(app)
def init_app(self, app):
for k, v in iteritems(self._default_config(app)):
app.config.setdefault(k, v)
if not app.config['DEBUG_TB_ENABLED']:
return
if not app.config.get('SECRET_KEY'):
raise RuntimeError(
"The Flask-DebugToolbar requires the 'SECRET_KEY' config "
"var to be set")
DebugToolbar.load_panels(app)
app.before_request(self.process_request)
app.after_request(self.process_response)
app.teardown_request(self.teardown_request)
# Monkey-patch the Flask.dispatch_request method
app.dispatch_request = self.dispatch_request
app.add_url_rule('/_debug_toolbar/static/<path:filename>',
'_debug_toolbar.static', self.send_static_file)
app.register_blueprint(module, url_prefix='/_debug_toolbar/views')
def _default_config(self, app):
return {
'DEBUG_TB_ENABLED': app.debug,
'DEBUG_TB_HOSTS': (),
'DEBUG_TB_INTERCEPT_REDIRECTS': True,
'DEBUG_TB_PANELS': (
'flask_debugtoolbar.panels.versions.VersionDebugPanel',
'flask_debugtoolbar.panels.timer.TimerDebugPanel',
'flask_debugtoolbar.panels.headers.HeaderDebugPanel',
'flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel',
'flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel',
'flask_debugtoolbar.panels.template.TemplateDebugPanel',
'flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel',
'flask_debugtoolbar.panels.logger.LoggingPanel',
'flask_debugtoolbar.panels.profiler.ProfilerDebugPanel',
),
}
def dispatch_request(self):
"""Modified version of Flask.dispatch_request to call process_view."""
req = _request_ctx_stack.top.request
app = current_app
if req.routing_exception is not None:
app.raise_routing_exception(req)
rule = req.url_rule
# if we provide automatic options for this URL and the
# request came with the OPTIONS method, reply automatically
if getattr(rule, 'provide_automatic_options', False) \
and req.method == 'OPTIONS':
return app.make_default_options_response()
# otherwise dispatch to the handler for that endpoint
view_func = app.view_functions[rule.endpoint]
view_func = self.process_view(app, view_func, req.view_args)
return view_func(**req.view_args)
def _show_toolbar(self):
"""Return a boolean to indicate if we need to show the toolbar."""
if request.blueprint == 'debugtoolbar':
return False
hosts = current_app.config['DEBUG_TB_HOSTS']
if hosts and request.remote_addr not in hosts:
return False
return True
def send_static_file(self, filename):
"""Send a static file from the flask-debugtoolbar static directory."""
return send_from_directory(self._static_dir, filename)
def process_request(self):
g.debug_toolbar = self
if not self._show_toolbar():
return
real_request = request._get_current_object()
self.debug_toolbars[real_request] = DebugToolbar(real_request, self.jinja_env)
for panel in self.debug_toolbars[real_request].panels:
panel.process_request(real_request)
def process_view(self, app, view_func, view_kwargs):
""" This method is called just before the flask view is called.
This is done by the dispatch_request method.
"""
real_request = request._get_current_object()
if real_request in self.debug_toolbars:
for panel in self.debug_toolbars[real_request].panels:
new_view = panel.process_view(real_request, view_func, view_kwargs)
if new_view:
view_func = new_view
return view_func
def process_response(self, response):
real_request = request._get_current_object()
if real_request not in self.debug_toolbars:
return response
# Intercept http redirect codes and display an html page with a
# link to the target.
if current_app.config['DEBUG_TB_INTERCEPT_REDIRECTS']:
if (response.status_code in self._redirect_codes and
not real_request.is_xhr):
redirect_to = response.location
redirect_code = response.status_code
if redirect_to:
content = self.render('redirect.html', {
'redirect_to': redirect_to,
'redirect_code': redirect_code
})
response.content_length = len(content)
response.location = None
response.response = [content]
response.status_code = 200
# If the http response code is 200 then we process to add the
# toolbar to the returned html response.
if (response.status_code == 200 and
response.headers['content-type'].startswith('text/html')):
for panel in self.debug_toolbars[real_request].panels:
panel.process_response(real_request, response)
if response.is_sequence:
response_html = response.data.decode(response.charset)
toolbar_html = self.debug_toolbars[real_request].render_toolbar()
content = replace_insensitive(
response_html, '</body>', toolbar_html + '</body>')
content = content.encode(response.charset)
response.response = [content]
response.content_length = len(content)
return response
def teardown_request(self, exc):
self.debug_toolbars.pop(request._get_current_object(), None)
def render(self, template_name, context):
template = self.jinja_env.get_template(template_name)
return template.render(**context)

View File

@@ -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())

View File

@@ -1,59 +0,0 @@
"""Base DebugPanel class"""
class DebugPanel(object):
"""
Base class for debug panels.
"""
# name = Base
has_content = False # If content returns something, set to true in subclass
# If the client is able to activate/de-activate the panel
user_enable = False
# We'll maintain a local context instance so we can expose our template
# context variables to panels which need them:
context = {}
# Panel methods
def __init__(self, jinja_env, context={}):
self.context.update(context)
self.jinja_env = jinja_env
# If the client enabled the panel
self.is_active = False
def render(self, template_name, context):
template = self.jinja_env.get_template(template_name)
return template.render(**context)
def dom_id(self):
return 'flDebug%sPanel' % (self.name.replace(' ', ''))
def nav_title(self):
"""Title showing in toolbar"""
raise NotImplementedError
def nav_subtitle(self):
"""Subtitle showing until title in toolbar"""
return ''
def title(self):
"""Title showing in panel"""
raise NotImplementedError
def url(self):
raise NotImplementedError
def content(self):
raise NotImplementedError
# Standard middleware methods
def process_request(self, request):
pass
def process_view(self, request, view_func, view_kwargs):
pass
def process_response(self, request, response):
pass

View File

@@ -1,29 +0,0 @@
from flask import current_app
from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class ConfigVarsDebugPanel(DebugPanel):
"""
A panel to display all variables from Flask configuration
"""
name = 'ConfigVars'
has_content = True
def nav_title(self):
return _('Config')
def title(self):
return _('Config')
def url(self):
return ''
def content(self):
context = self.context.copy()
context.update({
'config': current_app.config,
})
return self.render('panels/config_vars.html', context)

View File

@@ -1,56 +0,0 @@
from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class HeaderDebugPanel(DebugPanel):
"""
A panel to display HTTP headers.
"""
name = 'Header'
has_content = True
# List of headers we want to display
header_filter = (
'CONTENT_TYPE',
'HTTP_ACCEPT',
'HTTP_ACCEPT_CHARSET',
'HTTP_ACCEPT_ENCODING',
'HTTP_ACCEPT_LANGUAGE',
'HTTP_CACHE_CONTROL',
'HTTP_CONNECTION',
'HTTP_HOST',
'HTTP_KEEP_ALIVE',
'HTTP_REFERER',
'HTTP_USER_AGENT',
'QUERY_STRING',
'REMOTE_ADDR',
'REMOTE_HOST',
'REQUEST_METHOD',
'SCRIPT_NAME',
'SERVER_NAME',
'SERVER_PORT',
'SERVER_PROTOCOL',
'SERVER_SOFTWARE',
)
def nav_title(self):
return _('HTTP Headers')
def title(self):
return _('HTTP Headers')
def url(self):
return ''
def process_request(self, request):
self.headers = dict(
[(k, request.environ[k])
for k in self.header_filter if k in request.environ]
)
def content(self):
context = self.context.copy()
context.update({
'headers': self.headers
})
return self.render('panels/headers.html', context)

View File

@@ -1,115 +0,0 @@
from __future__ import with_statement
import datetime
import logging
try:
import threading
except ImportError:
threading = None
from flask_debugtoolbar.panels import DebugPanel
from flask_debugtoolbar.utils import format_fname
_ = lambda x: x
class ThreadTrackingHandler(logging.Handler):
def __init__(self):
if threading is None:
raise NotImplementedError("threading module is not available, \
the logging panel cannot be used without it")
logging.Handler.__init__(self)
self.records = {} # a dictionary that maps threads to log records
def emit(self, record):
self.get_records().append(record)
def get_records(self, thread=None):
"""
Returns a list of records for the provided thread, of if none is provided,
returns a list for the current thread.
"""
if thread is None:
thread = threading.currentThread()
if thread not in self.records:
self.records[thread] = []
return self.records[thread]
def clear_records(self, thread=None):
if thread is None:
thread = threading.currentThread()
if thread in self.records:
del self.records[thread]
handler = None
_init_lock = threading.Lock()
def _init_once():
global handler
if handler is not None:
return
with _init_lock:
if handler is not None:
return
# Call werkzeug's internal logging to make sure it gets configured
# before we add our handler. Otherwise werkzeug will see our handler
# and not configure console logging for the request log.
# Werkzeug's default log level is INFO so this message probably won't be
# seen.
try:
from werkzeug._internal import _log
except ImportError:
pass
else:
_log('debug', 'Initializing Flask-DebugToolbar log handler')
handler = ThreadTrackingHandler()
logging.root.addHandler(handler)
class LoggingPanel(DebugPanel):
name = 'Logging'
has_content = True
def process_request(self, request):
_init_once()
handler.clear_records()
def get_and_delete(self):
records = handler.get_records()
handler.clear_records()
return records
def nav_title(self):
return _("Logging")
def nav_subtitle(self):
# FIXME l10n: use ngettext
return "%s message%s" % \
(len(handler.get_records()), (len(handler.get_records()) == 1) and '' or 's')
def title(self):
return _('Log Messages')
def url(self):
return ''
def content(self):
records = []
for record in self.get_and_delete():
records.append({
'message': record.getMessage(),
'time': datetime.datetime.fromtimestamp(record.created),
'level': record.levelname,
'file': format_fname(record.pathname),
'file_long': record.pathname,
'line': record.lineno,
})
context = self.context.copy()
context.update({'records': records})
return self.render('panels/logger.html', context)

View File

@@ -1,120 +0,0 @@
import sys
try:
import cProfile as profile
except ImportError:
import profile
import functools
import os.path
import pstats
from flask import current_app
from flask_debugtoolbar.panels import DebugPanel
from flask_debugtoolbar.utils import format_fname
class ProfilerDebugPanel(DebugPanel):
"""
Panel that displays the time a response took with cProfile output.
"""
name = 'Profiler'
user_activate = True
def __init__(self, jinja_env, context={}):
DebugPanel.__init__(self, jinja_env, context=context)
if current_app.config.get('DEBUG_TB_PROFILER_ENABLED'):
self.is_active = True
def has_content(self):
return bool(self.profiler)
def process_request(self, request):
if not self.is_active:
return
self.profiler = profile.Profile()
self.stats = None
def process_view(self, request, view_func, view_kwargs):
if self.is_active:
func = functools.partial(self.profiler.runcall, view_func)
functools.update_wrapper(func, view_func)
return func
def process_response(self, request, response):
if not self.is_active:
return False
if self.profiler is not None:
self.profiler.disable()
try:
stats = pstats.Stats(self.profiler)
except TypeError:
self.is_active = False
return False
function_calls = []
for func in stats.sort_stats(1).fcn_list:
current = {}
info = stats.stats[func]
# Number of calls
if info[0] != info[1]:
current['ncalls'] = '%d/%d' % (info[1], info[0])
else:
current['ncalls'] = info[1]
# Total time
current['tottime'] = info[2] * 1000
# Quotient of total time divided by number of calls
if info[1]:
current['percall'] = info[2] * 1000 / info[1]
else:
current['percall'] = 0
# Cumulative time
current['cumtime'] = info[3] * 1000
# Quotient of the cumulative time divded by the number of
# primitive calls.
if info[0]:
current['percall_cum'] = info[3] * 1000 / info[0]
else:
current['percall_cum'] = 0
# Filename
filename = pstats.func_std_string(func)
current['filename_long'] = filename
current['filename'] = format_fname(filename)
function_calls.append(current)
self.stats = stats
self.function_calls = function_calls
# destroy the profiler just in case
return response
def title(self):
if not self.is_active:
return "Profiler not active"
return 'View: %.2fms' % (float(self.stats.total_tt)*1000,)
def nav_title(self):
return 'Profiler'
def nav_subtitle(self):
if not self.is_active:
return "in-active"
return 'View: %.2fms' % (float(self.stats.total_tt)*1000,)
def url(self):
return ''
def content(self):
if not self.is_active:
return "The profiler is not activated, activate it to use it"
context = {
'stats': self.stats,
'function_calls': self.function_calls,
}
return self.render('panels/profiler.html', context)

View File

@@ -1,48 +0,0 @@
from flask import session
from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class RequestVarsDebugPanel(DebugPanel):
"""
A panel to display request variables (POST/GET, session, cookies).
"""
name = 'RequestVars'
has_content = True
def nav_title(self):
return _('Request Vars')
def title(self):
return _('Request Vars')
def url(self):
return ''
def process_request(self, request):
self.request = request
self.session = session
self.view_func = None
self.view_args = []
self.view_kwargs = {}
def process_view(self, request, view_func, view_kwargs):
self.view_func = view_func
self.view_kwargs = view_kwargs
def content(self):
context = self.context.copy()
context.update({
'get': [(k, self.request.args.getlist(k)) for k in self.request.args],
'post': [(k, self.request.form.getlist(k)) for k in self.request.form],
'cookies': [(k, self.request.cookies.get(k)) for k in self.request.cookies],
'view_func': ('%s.%s' % (self.view_func.__module__, self.view_func.__name__)
if self.view_func else '[unknown]'),
'view_args': self.view_args,
'view_kwargs': self.view_kwargs or {},
'session': self.session.items(),
})
return self.render('panels/request_vars.html', context)

View File

@@ -1,137 +0,0 @@
try:
from flask.ext.sqlalchemy import get_debug_queries, SQLAlchemy
except ImportError:
sqlalchemy_available = False
get_debug_queries = SQLAlchemy = None
else:
sqlalchemy_available = True
from flask import request, current_app, abort, json_available, g
from flask_debugtoolbar import module
from flask_debugtoolbar.panels import DebugPanel
from flask_debugtoolbar.utils import format_fname, format_sql
import itsdangerous
_ = lambda x: x
def query_signer():
return itsdangerous.URLSafeSerializer(current_app.config['SECRET_KEY'],
salt='fdt-sql-query')
def dump_query(statement, params):
if not params or not statement.lower().strip().startswith('select'):
return None
try:
return query_signer().dumps([statement, params])
except TypeError:
return None
def load_query(data):
try:
statement, params = query_signer().loads(request.args['query'])
except (itsdangerous.BadSignature, TypeError):
abort(406)
# Make sure it is a select statement
if not statement.lower().strip().startswith('select'):
abort(406)
return statement, params
class SQLAlchemyDebugPanel(DebugPanel):
"""
Panel that displays the time a response took in milliseconds.
"""
name = 'SQLAlchemy'
@property
def has_content(self):
if not json_available or not sqlalchemy_available:
return True # will display an error message
return bool(get_debug_queries())
def process_request(self, request):
pass
def process_response(self, request, response):
pass
def nav_title(self):
return _('SQLAlchemy')
def nav_subtitle(self):
if not json_available or not sqlalchemy_available:
return 'Unavailable'
if get_debug_queries:
count = len(get_debug_queries())
return "%d %s" % (count, "query" if count == 1 else "queries")
def title(self):
return _('SQLAlchemy queries')
def url(self):
return ''
def content(self):
if not json_available or not sqlalchemy_available:
msg = ['Missing required libraries:', '<ul>']
if not json_available:
msg.append('<li>simplejson</li>')
if not sqlalchemy_available:
msg.append('<li>Flask-SQLAlchemy</li>')
msg.append('</ul>')
return '\n'.join(msg)
queries = get_debug_queries()
data = []
for query in queries:
data.append({
'duration': query.duration,
'sql': format_sql(query.statement, query.parameters),
'signed_query': dump_query(query.statement, query.parameters),
'context_long': query.context,
'context': format_fname(query.context)
})
return self.render('panels/sqlalchemy.html', {'queries': data})
# Panel views
@module.route('/sqlalchemy/sql_select', methods=['GET', 'POST'])
def sql_select():
statement, params = load_query(request.args['query'])
engine = SQLAlchemy().get_engine(current_app)
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']),
})
@module.route('/sqlalchemy/sql_explain', methods=['GET', 'POST'])
def sql_explain():
statement, params = load_query(request.args['query'])
engine = SQLAlchemy().get_engine(current_app)
if engine.driver == 'pysqlite':
query = 'EXPLAIN QUERY PLAN %s' % statement
else:
query = 'EXPLAIN %s' % statement
result = engine.execute(query, params)
return g.debug_toolbar.render('panels/sqlalchemy_explain.html', {
'result': result.fetchall(),
'headers': result.keys(),
'sql': format_sql(statement, params),
'duration': float(request.args['duration']),
})

View File

@@ -1,136 +0,0 @@
import collections
import json
import sys
import traceback
import uuid
from jinja2.exceptions import TemplateSyntaxError
from flask import (
template_rendered, request, g, render_template_string,
Response, current_app, abort, url_for
)
from flask_debugtoolbar import module
from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class TemplateDebugPanel(DebugPanel):
"""
Panel that displays the time a response took in milliseconds.
"""
name = 'Template'
has_content = True
# save the context for the 5 most recent requests
template_cache = collections.deque(maxlen=5)
@classmethod
def get_cache_for_key(self, key):
for cache_key, value in self.template_cache:
if key == cache_key:
return value
raise KeyError(key)
def __init__(self, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
self.key = str(uuid.uuid4())
self.templates = []
template_rendered.connect(self._store_template_info)
def _store_template_info(self, sender, **kwargs):
# only record in the cache if the editor is enabled and there is
# actually a template for this request
if not self.templates and is_editor_enabled():
self.template_cache.append((self.key, self.templates))
self.templates.append(kwargs)
def process_request(self, request):
pass
def process_response(self, request, response):
pass
def nav_title(self):
return _('Templates')
def nav_subtitle(self):
return "%d rendered" % len(self.templates)
def title(self):
return _('Templates')
def url(self):
return ''
def content(self):
return self.render('panels/template.html', {
'key': self.key,
'templates': self.templates,
'editable': is_editor_enabled(),
})
def is_editor_enabled():
return current_app.config.get('DEBUG_TB_TEMPLATE_EDITOR_ENABLED')
def require_enabled():
if not is_editor_enabled():
abort(403)
def _get_source(template):
with open(template.filename, 'rb') as fp:
source = fp.read()
return source.decode(_template_encoding())
def _template_encoding():
return getattr(current_app.jinja_loader, 'encoding', 'utf-8')
@module.route('/template/<key>')
def template_editor(key):
require_enabled()
# TODO set up special loader that caches templates it loads
# and can override template contents
templates = [t['template'] for t in TemplateDebugPanel.get_cache_for_key(key)]
return g.debug_toolbar.render('panels/template_editor.html', {
'static_path': url_for('_debug_toolbar.static', filename=''),
'request': request,
'templates': [
{'name': t.name, 'source': _get_source(t)}
for t in templates
]
})
@module.route('/template/<key>/save', methods=['POST'])
def save_template(key):
require_enabled()
template = TemplateDebugPanel.get_cache_for_key(key)[0]['template']
content = request.form['content'].encode(_template_encoding())
with open(template.filename, 'wb') as fp:
fp.write(content)
return 'ok'
@module.route('/template/<key>', methods=['POST'])
def template_preview(key):
require_enabled()
context = TemplateDebugPanel.get_cache_for_key(key)[0]['context']
content = request.form['content']
env = current_app.jinja_env.overlay(autoescape=True)
try:
template = env.from_string(content)
return template.render(context)
except Exception as e:
tb = sys.exc_info()[2]
try:
while tb.tb_next:
tb = tb.tb_next
msg = {'lineno': tb.tb_lineno, 'error': str(e)}
return Response(json.dumps(msg), status=400, mimetype='application/json')
finally:
del tb

View File

@@ -1,94 +0,0 @@
try:
import resource
except ImportError:
pass # Will fail on Win32 systems
import time
from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class TimerDebugPanel(DebugPanel):
"""
Panel that displays the time a response took in milliseconds.
"""
name = 'Timer'
try: # if resource module not available, don't show content panel
resource
except NameError:
has_content = False
has_resource = False
else:
has_content = True
has_resource = True
def process_request(self, request):
self._start_time = time.time()
if self.has_resource:
self._start_rusage = resource.getrusage(resource.RUSAGE_SELF)
def process_response(self, request, response):
self.total_time = (time.time() - self._start_time) * 1000
if self.has_resource:
self._end_rusage = resource.getrusage(resource.RUSAGE_SELF)
def nav_title(self):
return _('Time')
def nav_subtitle(self):
# TODO l10n
if self.has_resource:
utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime
stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime
return 'CPU: %0.2fms (%0.2fms)' % ((utime + stime) * 1000.0, self.total_time)
else:
return 'TOTAL: %0.2fms' % (self.total_time)
def title(self):
return _('Resource Usage')
def url(self):
return ''
def _elapsed_ru(self, name):
return getattr(self._end_rusage, name) - getattr(self._start_rusage, name)
def content(self):
utime = 1000 * self._elapsed_ru('ru_utime')
stime = 1000 * self._elapsed_ru('ru_stime')
vcsw = self._elapsed_ru('ru_nvcsw')
ivcsw = self._elapsed_ru('ru_nivcsw')
minflt = self._elapsed_ru('ru_minflt')
majflt = self._elapsed_ru('ru_majflt')
# these are documented as not meaningful under Linux. If you're running BSD
# feel free to enable them, and add any others that I hadn't gotten to before
# I noticed that I was getting nothing but zeroes and that the docs agreed. :-(
#
# blkin = self._elapsed_ru('ru_inblock')
# blkout = self._elapsed_ru('ru_oublock')
# swap = self._elapsed_ru('ru_nswap')
# rss = self._end_rusage.ru_maxrss
# srss = self._end_rusage.ru_ixrss
# urss = self._end_rusage.ru_idrss
# usrss = self._end_rusage.ru_isrss
# TODO l10n on values
rows = (
(_('User CPU time'), '%0.3f msec' % utime),
(_('System CPU time'), '%0.3f msec' % stime),
(_('Total CPU time'), '%0.3f msec' % (utime + stime)),
(_('Elapsed time'), '%0.3f msec' % self.total_time),
(_('Context switches'), '%d voluntary, %d involuntary' % (vcsw, ivcsw)),
# ('Memory use', '%d max RSS, %d shared, %d unshared' % (rss, srss, urss + usrss)),
# ('Page faults', '%d no i/o, %d requiring i/o' % (minflt, majflt)),
# ('Disk operations', '%d in, %d out, %d swapout' % (blkin, blkout, swap)),
)
context = self.context.copy()
context.update({
'rows': rows,
})
return self.render('panels/timer.html', context)

View File

@@ -1,27 +0,0 @@
from flask import __version__ as flask_version
from flask_debugtoolbar.panels import DebugPanel
_ = lambda x: x
class VersionDebugPanel(DebugPanel):
"""
Panel that displays the Flask version.
"""
name = 'Version'
has_content = False
def nav_title(self):
return _('Versions')
def nav_subtitle(self):
return 'Flask %s' % flask_version
def url(self):
return ''
def title(self):
return _('Versions')
def content(self):
return None

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +0,0 @@
<table>
<thead>
<tr>
<th>&nbsp;(ms)</th>
<th>Action</th>
<th>Context</th>
<th>Query</th>
</tr>
</thead>
<tbody>
{% for query in queries %}
<tr class="{{ loop.cycle('flDebugOdd', 'flDebugEven') }}">
<td>{{ '%.4f'|format(query.duration * 1000) }}</td>
<td>
{% if query.signed_query %}
<a class="remoteCall" href="/_debug_toolbar/views/sqlalchemy/sql_select?query={{ query.signed_query }}&amp;duration={{ query.duration|urlencode }}">SELECT</a><br />
<a class="remoteCall" href="/_debug_toolbar/views/sqlalchemy/sql_explain?query={{ query.signed_query }}&amp;duration={{ query.duration|urlencode }}">EXPLAIN</a><br />
{% endif %}
</td>
<td title="{{ query.context_long }}">
{{ query.context }}
</td>
<td class="syntax">
<div class="flDebugSqlWrap">
<div class="flDebugSql">{{ query.sql|safe }}</div>
{#
{% if query.stacktrace %}
<div class="djSQLHideStacktraceDiv" style="display:none;">
<table>
<tr>
<th>{% trans "Line" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "File" %}</th>
</tr>
{% for file, line, method in query.stacktrace %}
<tr>
<td>{{ line }}</td>
<td><code>{{ method|escape }}</code></td>
<td><code>{{ file|escape }}</code></td>
</tr>
{% endfor %}
</table>
{% if query.template_info %}
<table>
{% for line in query.template_info.context %}
<tr>
<td>{{ line.num }}</td>
<td><code style="font-family: monospace;{% if line.highlight %}background-color: lightgrey{% endif %}">{{ line.content }}</code></td>
</tr>
{% endfor %}
</table>
<p><strong>{{ query.template_info.name|default:"(unknown)" }}</strong></p>
{% endif %}
</div>
{% endif %}
<span class="djDebugLineChart{% if query.is_slow %} djDebugLineChartWarning{% endif %}" style="width:{{ query.width_ratio }}%; left:{{ query.start_offset }}%;"></span>
#}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

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

View File

@@ -1,76 +0,0 @@
try:
from urllib.parse import unquote
except ImportError:
from urllib import unquote
from flask import url_for, current_app
from werkzeug.utils import import_string
class DebugToolbar(object):
_cached_panel_classes = {}
def __init__(self, request, jinja_env):
self.jinja_env = jinja_env
self.request = request
self.panels = []
self.template_context = {
'static_path': url_for('_debug_toolbar.static', filename='')
}
self.create_panels()
def create_panels(self):
"""
Populate debug panels
"""
activated = self.request.cookies.get('fldt_active', '')
activated = unquote(activated).split(';')
for panel_class in self._iter_panels(current_app):
panel_instance = panel_class(jinja_env=self.jinja_env, context=self.template_context)
if panel_instance.dom_id() in activated:
panel_instance.is_active = True
self.panels.append(panel_instance)
def render_toolbar(self):
context = self.template_context.copy()
context.update({'panels': self.panels})
template = self.jinja_env.get_template('base.html')
return template.render(**context)
@classmethod
def load_panels(cls, app):
for panel_class in cls._iter_panels(app):
# just loop to make sure they've been loaded
pass
@classmethod
def _iter_panels(cls, app):
for panel_path in app.config['DEBUG_TB_PANELS']:
panel_class = cls._import_panel(app, panel_path)
if panel_class is not None:
yield panel_class
@classmethod
def _import_panel(cls, app, path):
cache = cls._cached_panel_classes
try:
return cache[path]
except KeyError:
pass
try:
panel_class = import_string(path)
except ImportError as e:
app.logger.warning('Disabled %s due to ImportError: %s', path, e)
panel_class = None
cache[path] = panel_class
return panel_class

View File

@@ -1,57 +0,0 @@
import os.path
import sys
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')
HAVE_PYGMENTS = True
except ImportError:
HAVE_PYGMENTS = False
from flask import current_app
def format_fname(value):
# If the value is not an absolute path, the it is a builtin or
# a relative file (thus a project file).
if not os.path.isabs(value):
if value.startswith(('{', '<')):
return value
if value.startswith('.' + os.path.sep):
return value
return '.' + os.path.sep + value
# If the file is absolute and within the project root handle it as
# a project file
if value.startswith(current_app.root_path):
return "." + value[len(current_app.root_path):]
# Loop through sys.path to find the longest match and return
# the relative path from there.
paths = sys.path
prefix = None
prefix_len = 0
for path in sys.path:
new_prefix = os.path.commonprefix([path, value])
if len(new_prefix) > prefix_len:
prefix = new_prefix
prefix_len = len(prefix)
if not prefix.endswith(os.path.sep):
prefix_len -= 1
path = value[prefix_len:]
return '<%s>' % path
def format_sql(query, args):
if not HAVE_PYGMENTS:
return query
return highlight(
query,
SqlLexer(encoding='utf-8'),
HtmlFormatter(encoding='utf-8', noclasses=True, style=PYGMENT_STYLE))

84
pyproject.toml Normal file
View File

@@ -0,0 +1,84 @@
[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",
]
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
View File

@@ -0,0 +1 @@
build

18
requirements/build.txt Normal file
View 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
View File

@@ -0,0 +1,5 @@
-r docs.txt
-r tests.txt
-r typing.txt
pre-commit
tox

257
requirements/dev.txt Normal file
View 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
View File

@@ -0,0 +1,3 @@
pallets-sphinx-themes
sphinx
sphinxcontrib-log-cabinet

63
requirements/docs.txt Normal file
View 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
View File

@@ -0,0 +1,3 @@
pytest
flask-sqlalchemy
pygments

48
requirements/tests.txt Normal file
View 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
View File

@@ -0,0 +1,5 @@
mypy
pyright
pytest
types-pygments
flask-sqlalchemy

68
requirements/typing.txt Normal file
View 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

View File

@@ -1,48 +0,0 @@
import os
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
try:
README = open(os.path.join(here, 'README.rst')).read()
CHANGES = open(os.path.join(here, 'CHANGES.rst')).read()
except:
README = ''
CHANGES = ''
setup(
name='Flask-DebugToolbar',
version='0.9.0',
url='http://flask-debugtoolbar.rtfd.org/',
license='BSD',
author='Michael van Tellingen',
author_email='michaelvantellingen@gmail.com',
maintainer='Matt Good',
maintainer_email='matt@matt-good.net',
description='A port of the Django debug toolbar to Flask',
long_description=README + '\n\n' + CHANGES,
zip_safe=False,
platforms='any',
include_package_data=True,
packages=['flask_debugtoolbar',
'flask_debugtoolbar.panels'
],
install_requires=[
'Flask>=0.8',
'Blinker',
'itsdangerous',
'werkzeug',
],
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'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'
]
)

View File

@@ -0,0 +1,359 @@
from __future__ import annotations
import collections.abc as c
import os
import typing as t
import urllib.parse
import warnings
from contextvars import ContextVar
import jinja2.ext
from flask import Blueprint
from flask import current_app
from flask import Flask
from flask import g
from flask import request
from flask import send_from_directory
from flask import url_for
from flask.globals import request_ctx
from jinja2 import Environment
from jinja2 import PackageLoader
from werkzeug import Request
from werkzeug import Response
from werkzeug.routing import Rule
from .toolbar import DebugToolbar
from .utils import decode_text
from .utils import gzip_compress
from .utils import gzip_decompress
module: Blueprint = Blueprint("debugtoolbar", __name__)
def replace_insensitive(string: str, target: str, replacement: str) -> str:
"""Similar to string.replace() but is case insensitive
Code borrowed from:
http://forums.devshed.com/python-programming-11/case-insensitive-string-replace-490921.html
"""
no_case = string.lower()
index = no_case.rfind(target.lower())
if index >= 0:
return string[:index] + replacement + string[index + len(target) :]
else: # no results so return the original string
return string
def _printable(value: object) -> str:
try:
return decode_text(repr(value))
except Exception as e:
return f"<repr({object.__repr__(value)}) raised {type(e).__name__}: {e}>"
class DebugToolbarExtension:
_static_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), "static"))
_toolbar_codes = [200, 201, 400, 401, 403, 404, 405, 500, 501, 502, 503, 504]
_redirect_codes = [301, 302, 303, 304]
def __init__(self, app: Flask | None = None) -> None:
self.app = app
self.toolbar_routes_host: str | None = None
# Support threads running `flask.copy_current_request_context` without
# poping toolbar during `teardown_request`
self.debug_toolbars_var: ContextVar[dict[Request, DebugToolbar]] = ContextVar(
"debug_toolbars"
)
jinja_extensions = [jinja2.ext.i18n]
# Jinja2<3
if hasattr(jinja2.ext, "with_"):
jinja_extensions.append(jinja2.ext.with_) # pyright: ignore
# Configure jinja for the internal templates and add url rules
# for static data
self.jinja_env: Environment = Environment(
autoescape=True,
extensions=jinja_extensions,
loader=PackageLoader(__name__, "templates"),
)
self.jinja_env.filters["urlencode"] = urllib.parse.quote_plus
self.jinja_env.filters["printable"] = _printable
self.jinja_env.globals["url_for"] = url_for
if app is not None:
self.init_app(app)
def init_app(self, app: Flask) -> None:
for k, v in self._default_config(app).items():
app.config.setdefault(k, v)
if not app.config["DEBUG_TB_ENABLED"]:
return
if not app.config.get("SECRET_KEY"):
raise RuntimeError(
"The Flask-DebugToolbar requires the 'SECRET_KEY' config "
"var to be set"
)
self._validate_and_configure_toolbar_routes_host(app)
DebugToolbar.load_panels(app)
app.before_request(self.process_request)
app.after_request(self.process_response)
app.teardown_request(self.teardown_request)
# Monkey-patch the Flask.dispatch_request method
app.dispatch_request = self.dispatch_request # type: ignore[method-assign]
app.add_url_rule(
"/_debug_toolbar/static/<path:filename>",
"_debug_toolbar.static",
self.send_static_file,
host=self.toolbar_routes_host,
)
app.register_blueprint(module, url_prefix="/_debug_toolbar/views")
def _default_config(self, app: Flask) -> dict[str, t.Any]:
return {
"DEBUG_TB_ENABLED": app.debug,
"DEBUG_TB_HOSTS": (),
"DEBUG_TB_ROUTES_HOST": None,
"DEBUG_TB_INTERCEPT_REDIRECTS": True,
"DEBUG_TB_PANELS": (
"flask_debugtoolbar.panels.versions.VersionDebugPanel",
"flask_debugtoolbar.panels.timer.TimerDebugPanel",
"flask_debugtoolbar.panels.headers.HeaderDebugPanel",
"flask_debugtoolbar.panels.request_vars.RequestVarsDebugPanel",
"flask_debugtoolbar.panels.config_vars.ConfigVarsDebugPanel",
"flask_debugtoolbar.panels.template.TemplateDebugPanel",
"flask_debugtoolbar.panels.sqlalchemy.SQLAlchemyDebugPanel",
"flask_debugtoolbar.panels.logger.LoggingPanel",
"flask_debugtoolbar.panels.route_list.RouteListDebugPanel",
"flask_debugtoolbar.panels.profiler.ProfilerDebugPanel",
"flask_debugtoolbar.panels.g.GDebugPanel",
),
"SQLALCHEMY_RECORD_QUERIES": app.debug,
}
def _validate_and_configure_toolbar_routes_host(self, app: Flask) -> None:
toolbar_routes_host = app.config["DEBUG_TB_ROUTES_HOST"]
if app.url_map.host_matching and not toolbar_routes_host:
import warnings
warnings.warn(
"Flask-DebugToolbar requires DEBUG_TB_ROUTES_HOST to be set if Flask "
"is running in `host_matching` mode. Static assets for the toolbar "
"will not be served correctly unless this is set.",
stacklevel=1,
)
if toolbar_routes_host:
if not app.url_map.host_matching:
raise ValueError(
"`DEBUG_TB_ROUTES_HOST` should only be set if your Flask app is "
"using `host_matching`."
)
if toolbar_routes_host.strip() == "*":
toolbar_routes_host = "<toolbar_routes_host>"
elif "<" in toolbar_routes_host and ">" in toolbar_routes_host:
raise ValueError(
"`DEBUG_TB_ROUTES_HOST` must either be a host name with no "
"variables, to serve all Flask-DebugToolbar assets from a single "
"host, or `*` to match the current request's host."
)
# Automatically inject `toolbar_routes_host` into `url_for` calls for
# the toolbar's `send_static_file` method.
@app.url_defaults
def inject_toolbar_routes_host_if_required(
endpoint: str, values: dict[str, t.Any]
) -> None:
if app.url_map.is_endpoint_expecting(endpoint, "toolbar_routes_host"):
values.setdefault("toolbar_routes_host", request.host)
# Automatically strip `toolbar_routes_host` from the endpoint values so
# that the `send_static_host` method doesn't receive that parameter,
# as it's not actually required internally.
@app.url_value_preprocessor
def strip_toolbar_routes_host_from_static_endpoint(
endpoint: str | None, values: dict[str, t.Any] | None
) -> None:
if (
endpoint
and values
and app.url_map.is_endpoint_expecting(
endpoint, "toolbar_routes_host"
)
):
values.pop("toolbar_routes_host", None)
self.toolbar_routes_host = toolbar_routes_host
def dispatch_request(self) -> t.Any:
"""Modified version of ``Flask.dispatch_request`` to call
:meth:`process_view`.
"""
# self references this extension, use current_app to call app methods.
app = current_app._get_current_object() # type: ignore[attr-defined]
req = request_ctx.request
if req.routing_exception is not None:
app.raise_routing_exception(req)
rule: Rule = req.url_rule # type: ignore[assignment]
if (
getattr(rule, "provide_automatic_options", False)
and req.method == "OPTIONS"
):
return app.make_default_options_response()
view_func = app.view_functions[rule.endpoint]
view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment]
# allow each toolbar to process the view and args
view_func = self.process_view(app, view_func, view_args)
return view_func(**view_args)
def _show_toolbar(self) -> bool:
"""Return a boolean to indicate if we need to show the toolbar."""
if request.blueprint == "debugtoolbar":
return False
hosts = current_app.config["DEBUG_TB_HOSTS"]
if hosts and request.remote_addr not in hosts:
return False
return True
def send_static_file(self, filename: str) -> Response:
"""Send a static file from the flask-debugtoolbar static directory."""
return send_from_directory(self._static_dir, filename)
def process_request(self) -> None:
g.debug_toolbar = self
if not self._show_toolbar():
return
real_request = request._get_current_object() # type: ignore[attr-defined]
self.debug_toolbars_var.set({})
self.debug_toolbars_var.get()[real_request] = DebugToolbar(
real_request, self.jinja_env
)
for panel in self.debug_toolbars_var.get()[real_request].panels:
panel.process_request(real_request)
def process_view(
self,
app: Flask,
view_func: c.Callable[..., t.Any],
view_kwargs: dict[str, t.Any],
) -> c.Callable[..., t.Any]:
"""This method is called just before the flask view is called.
This is done by the dispatch_request method.
"""
real_request = request._get_current_object() # type: ignore[attr-defined]
try:
toolbar = self.debug_toolbars_var.get({})[real_request]
except KeyError:
return view_func
for panel in toolbar.panels:
new_view = panel.process_view(real_request, view_func, view_kwargs)
if new_view:
view_func = new_view
return view_func
def process_response(self, response: Response) -> Response:
real_request = request._get_current_object() # type: ignore[attr-defined]
if real_request not in self.debug_toolbars_var.get({}):
return response
# Intercept http redirect codes and display an html page with a
# link to the target.
if current_app.config["DEBUG_TB_INTERCEPT_REDIRECTS"]:
if response.status_code in self._redirect_codes:
redirect_to = response.location
redirect_code = response.status_code
if redirect_to:
content = self.render(
"redirect.html",
{"redirect_to": redirect_to, "redirect_code": redirect_code},
)
response.content_length = len(content)
del response.location
response.response = [content]
response.status_code = 200
# If the http response code is an allowed code then we process to add the
# toolbar to the returned html response.
if not (
response.status_code in self._toolbar_codes
and response.is_sequence
and response.headers["content-type"].startswith("text/html")
):
return response
content_encoding = response.headers.get("Content-Encoding")
if content_encoding and "gzip" in content_encoding:
response_html = gzip_decompress(response.data).decode()
else:
response_html = response.get_data(as_text=True)
no_case = response_html.lower()
body_end = no_case.rfind("</body>")
if body_end >= 0:
before = response_html[:body_end]
after = response_html[body_end:]
elif no_case.startswith("<!doctype html>"):
before = response_html
after = ""
else:
warnings.warn(
"Could not insert debug toolbar." " </body> tag not found in response.",
stacklevel=1,
)
return response
toolbar = self.debug_toolbars_var.get()[real_request]
for panel in toolbar.panels:
panel.process_response(real_request, response)
toolbar_html = toolbar.render_toolbar()
content = "".join((before, toolbar_html, after))
content_bytes = content.encode("utf-8")
if content_encoding and "gzip" in content_encoding:
content_bytes = gzip_compress(content_bytes)
response.response = [content_bytes]
response.content_length = len(content_bytes)
return response
def teardown_request(self, exc: BaseException | None) -> None:
# debug_toolbars_var won't be set under `flask.copy_current_request_context`
real_request = request._get_current_object() # type: ignore[attr-defined]
self.debug_toolbars_var.get({}).pop(real_request, None)
def render(self, template_name: str, context: dict[str, t.Any]) -> str:
template = self.jinja_env.get_template(template_name)
return template.render(**context)

View File

@@ -0,0 +1,99 @@
from __future__ import annotations
import collections.abc as c
import typing as t
from flask import Flask
from jinja2 import Environment
from werkzeug import Request
from werkzeug import Response
class DebugPanel:
"""Base class for debug panels."""
name: str
# If content returns something, set to true in subclass
has_content = False
# If the client is able to activate/de-activate the panel
user_enable = False
# We'll maintain a local context instance so we can expose our template
# context variables to panels which need them:
context: dict[str, t.Any] = {}
# Panel methods
def __init__(
self, jinja_env: Environment, context: dict[str, t.Any] | None = None
) -> None:
if context is not None:
self.context.update(context)
self.jinja_env = jinja_env
# If the client enabled the panel
self.is_active = False
@classmethod
def init_app(cls, app: Flask) -> None:
"""Method that can be overridden by child classes.
Can be used for setting up additional URL-rules/routes.
Example::
class UMLDiagramPanel(DebugPanel):
@classmethod
def init_app(cls, app):
app.add_url_rule(
'/_flask_debugtoolbar_umldiagram/<path:filename>',
'_flask_debugtoolbar_umldiagram.serve_generated_image',
cls.serve_generated_image
)
@classmethod
def serve_generated_image(cls, app):
return Response(...)
"""
pass
def render(self, template_name: str, context: dict[str, t.Any]) -> str:
template = self.jinja_env.get_template(template_name)
return template.render(**context)
def dom_id(self) -> str:
return f"flDebug{self.name.replace(' ', '')}Panel"
def nav_title(self) -> str:
"""Title showing in toolbar"""
raise NotImplementedError
def nav_subtitle(self) -> str:
"""Subtitle showing until title in toolbar"""
return ""
def title(self) -> str:
"""Title showing in panel"""
raise NotImplementedError
def url(self) -> str:
raise NotImplementedError
def content(self) -> str:
raise NotImplementedError
# Standard middleware methods
def process_request(self, request: Request) -> None:
pass
def process_view(
self,
request: Request,
view_func: c.Callable[..., t.Any],
view_kwargs: dict[str, t.Any],
) -> c.Callable[..., t.Any] | None:
pass
def process_response(self, request: Request, response: Response) -> None:
pass

View File

@@ -0,0 +1,30 @@
from __future__ import annotations
from flask import current_app
from . import DebugPanel
class ConfigVarsDebugPanel(DebugPanel):
"""A panel to display all variables from Flask configuration."""
name = "ConfigVars"
has_content = True
def nav_title(self) -> str:
return "Config"
def title(self) -> str:
return "Config"
def url(self) -> str:
return ""
def content(self) -> str:
context = self.context.copy()
context.update(
{
"config": current_app.config,
}
)
return self.render("panels/config_vars.html", context)

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from flask import g
from . import DebugPanel
class GDebugPanel(DebugPanel):
"""A panel to display ``flask.g`` content."""
name = "g"
has_content = True
def nav_title(self) -> str:
return "flask.g"
def title(self) -> str:
return "flask.g content"
def url(self) -> str:
return ""
def content(self) -> str:
context = self.context.copy()
context.update({"g_content": g.__dict__})
return self.render("panels/g.html", context)

View File

@@ -0,0 +1,56 @@
from __future__ import annotations
import typing as t
from werkzeug import Request
from . import DebugPanel
class HeaderDebugPanel(DebugPanel):
"""A panel to display HTTP headers."""
name = "Header"
has_content = True
# List of headers we want to display
header_filter: tuple[str, ...] = (
"CONTENT_TYPE",
"HTTP_ACCEPT",
"HTTP_ACCEPT_CHARSET",
"HTTP_ACCEPT_ENCODING",
"HTTP_ACCEPT_LANGUAGE",
"HTTP_CACHE_CONTROL",
"HTTP_CONNECTION",
"HTTP_HOST",
"HTTP_KEEP_ALIVE",
"HTTP_REFERER",
"HTTP_USER_AGENT",
"QUERY_STRING",
"REMOTE_ADDR",
"REMOTE_HOST",
"REQUEST_METHOD",
"SCRIPT_NAME",
"SERVER_NAME",
"SERVER_PORT",
"SERVER_PROTOCOL",
"SERVER_SOFTWARE",
)
def nav_title(self) -> str:
return "HTTP Headers"
def title(self) -> str:
return "HTTP Headers"
def url(self) -> str:
return ""
def process_request(self, request: Request) -> None:
self.headers: dict[str, t.Any] = {
k: request.environ[k] for k in self.header_filter if k in request.environ
}
def content(self) -> str:
context = self.context.copy()
context.update({"headers": self.headers})
return self.render("panels/headers.html", context)

View File

@@ -0,0 +1,115 @@
from __future__ import annotations
import datetime
import logging
import threading
from werkzeug import Request
from ..utils import format_fname
from . import DebugPanel
class ThreadTrackingHandler(logging.Handler):
def __init__(self) -> None:
super().__init__()
# a dictionary that maps threads to log records
self.records: dict[threading.Thread, list[logging.LogRecord]] = {}
def emit(self, record: logging.LogRecord) -> None:
self.get_records().append(record)
def get_records(
self, thread: threading.Thread | None = None
) -> list[logging.LogRecord]:
"""
Returns a list of records for the provided thread, of if none is
provided, returns a list for the current thread.
"""
if thread is None:
thread = threading.current_thread()
if thread not in self.records:
self.records[thread] = []
return self.records[thread]
def clear_records(self, thread: threading.Thread | None = None) -> None:
if thread is None:
thread = threading.current_thread()
if thread in self.records:
del self.records[thread]
handler: ThreadTrackingHandler = None # type: ignore[assignment]
_init_lock = threading.Lock()
def _init_once() -> None:
global handler
if handler is not None:
return
with _init_lock:
if handler is not None:
return
# Call werkzeug's internal logging to make sure it gets configured
# before we add our handler. Otherwise werkzeug will see our handler
# and not configure console logging for the request log.
# Werkzeug's default log level is INFO so this message probably won't
# be seen.
from werkzeug._internal import _log
_log("debug", "Initializing Flask-DebugToolbar log handler")
handler = ThreadTrackingHandler()
logging.root.addHandler(handler)
class LoggingPanel(DebugPanel):
name = "Logging"
has_content = True
def process_request(self, request: Request) -> None:
_init_once()
handler.clear_records()
def get_and_delete(self) -> list[logging.LogRecord]:
records = handler.get_records()
handler.clear_records()
return records
def nav_title(self) -> str:
return "Logging"
def nav_subtitle(self) -> str:
num_records = len(handler.get_records())
plural = "message" if num_records == 1 else "messages"
return f"{num_records} {plural}"
def title(self) -> str:
return "Log Messages"
def url(self) -> str:
return ""
def content(self) -> str:
records = []
for record in self.get_and_delete():
records.append(
{
"message": record.getMessage(),
"time": datetime.datetime.fromtimestamp(record.created),
"level": record.levelname,
"file": format_fname(record.pathname),
"file_long": record.pathname,
"line": record.lineno,
}
)
context = self.context.copy()
context.update({"records": records})
return self.render("panels/logger.html", context)

View File

@@ -0,0 +1,158 @@
from __future__ import annotations
import collections.abc as c
import functools
import pstats
import typing as t
from flask import current_app
from jinja2 import Environment
from werkzeug import Request
from werkzeug import Response
from ..utils import format_fname
from . import DebugPanel
try:
import cProfile as profile
except ImportError:
import profile # type: ignore[no-redef]
class ProfilerDebugPanel(DebugPanel):
"""Panel that displays the time a response took with cProfile output."""
name = "Profiler"
user_activate = True
is_active: bool = False
dump_filename: str | None = None
profiler: profile.Profile
stats: pstats.Stats | None = None
function_calls: list[dict[str, t.Any]]
def __init__(
self, jinja_env: Environment, context: dict[str, t.Any] | None = None
) -> None:
super().__init__(jinja_env, context=context)
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"
)
@property
def has_content(self) -> bool: # type: ignore[override]
return bool(self.profiler)
def process_request(self, request: Request) -> None:
if not self.is_active:
return
self.profiler = profile.Profile() # pyright: ignore
self.stats = None
def process_view(
self,
request: Request,
view_func: c.Callable[..., t.Any],
view_kwargs: dict[str, t.Any],
) -> c.Callable[..., t.Any] | None:
if self.is_active:
func = functools.partial(self.profiler.runcall, view_func)
functools.update_wrapper(func, view_func)
return func
return None
def process_response(self, request: Request, response: Response) -> None:
if not self.is_active:
return
if self.profiler is not None:
self.profiler.disable() # pyright: ignore
try:
stats = pstats.Stats(self.profiler)
except TypeError:
self.is_active = False
return
function_calls: list[dict[str, t.Any]] = []
for func in stats.sort_stats(1).fcn_list: # type: ignore[attr-defined]
current: dict[str, t.Any] = {}
info = stats.stats[func] # type: ignore[attr-defined]
# Number of calls
if info[0] != info[1]:
current["ncalls"] = f"{info[1]}/{info[0]}"
else:
current["ncalls"] = info[1]
# Total time
current["tottime"] = info[2] * 1000
# Quotient of total time divided by number of calls
if info[1]:
current["percall"] = info[2] * 1000 / info[1]
else:
current["percall"] = 0
# Cumulative time
current["cumtime"] = info[3] * 1000
# Quotient of the cumulative time divided by the number of
# primitive calls.
if info[0]:
current["percall_cum"] = info[3] * 1000 / info[0]
else:
current["percall_cum"] = 0
# Filename
filename = pstats.func_std_string(func) # type: ignore[attr-defined]
current["filename_long"] = filename
current["filename"] = format_fname(filename)
function_calls.append(current)
self.stats = stats
self.function_calls = function_calls
if self.dump_filename:
if callable(self.dump_filename):
filename = self.dump_filename()
else:
filename = self.dump_filename
self.profiler.dump_stats(filename)
def title(self) -> str:
if not self.is_active:
return "Profiler not active"
return f"View: {float(self.stats.total_tt) * 1000:.2f}ms" # type: ignore[union-attr]
def nav_title(self) -> str:
return "Profiler"
def nav_subtitle(self) -> str:
if not self.is_active:
return "in-active"
return f"View: {float(self.stats.total_tt) * 1000:.2f}ms" # type: ignore[union-attr]
def url(self) -> str:
return ""
def content(self) -> str:
if not self.is_active:
return "The profiler is not activated, activate it to use it"
context = {
"stats": self.stats,
"function_calls": self.function_calls,
}
return self.render("panels/profiler.html", context)

View File

@@ -0,0 +1,59 @@
from __future__ import annotations
import collections.abc as c
import typing as t
from flask import session
from werkzeug import Request
from . import DebugPanel
class RequestVarsDebugPanel(DebugPanel):
"""A panel to display request variables (POST/GET, session, cookies)."""
name = "RequestVars"
has_content = True
def nav_title(self) -> str:
return "Request Vars"
def title(self) -> str:
return "Request Vars"
def url(self) -> str:
return ""
def process_request(self, request: Request) -> None:
self.request = request
self.session = session
self.view_func: c.Callable[..., t.Any] | None = None
self.view_kwargs: dict[str, t.Any] = {}
def process_view(
self,
request: Request,
view_func: c.Callable[..., t.Any],
view_kwargs: dict[str, t.Any],
) -> None:
self.view_func = view_func
self.view_kwargs = view_kwargs
def content(self) -> str:
context = self.context.copy()
context.update(
{
"get": self.request.args.lists(),
"post": self.request.form.lists(),
"cookies": self.request.cookies.items(),
"view_func": (
f"{self.view_func.__module__}.{self.view_func.__name__}"
if self.view_func
else "[unknown]"
),
"view_kwargs": self.view_kwargs or {},
"session": self.session.items(),
}
)
return self.render("panels/request_vars.html", context)

View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from flask import current_app
from werkzeug import Request
from werkzeug.routing import Rule
from . import DebugPanel
class RouteListDebugPanel(DebugPanel):
"""Panel that displays the URL routing rules."""
name = "RouteList"
has_content = True
routes: list[Rule] = []
def nav_title(self) -> str:
return "Route List"
def title(self) -> str:
return "Route List"
def url(self) -> str:
return ""
def nav_subtitle(self) -> str:
count = len(self.routes)
plural = "route" if count == 1 else "routes"
return f"{count} {plural}"
def process_request(self, request: Request) -> None:
self.routes = [
rule
for rule in current_app.url_map.iter_rules()
if not rule.rule.startswith("/_debug_toolbar")
]
def content(self) -> str:
return self.render(
"panels/route_list.html",
{
"routes": self.routes,
},
)

View File

@@ -0,0 +1,186 @@
from __future__ import annotations
import typing as t
import itsdangerous
from flask import abort
from flask import current_app
from flask import g
from flask import request
from .. import module
from ..utils import format_fname
from ..utils import format_sql
from . import DebugPanel
try:
from flask_sqlalchemy import SQLAlchemy
except ImportError:
sqlalchemy_available: bool = False
get_recorded_queries = SQLAlchemy = None # type: ignore[misc, assignment]
debug_enables_record_queries: bool = False
else:
try:
from flask_sqlalchemy.record_queries import ( # type: ignore[assignment]
get_recorded_queries,
)
debug_enables_record_queries = False
except ImportError:
# For flask_sqlalchemy < 3.0.0
from flask_sqlalchemy import ( # type: ignore[no-redef]
get_debug_queries as get_recorded_queries,
)
# flask_sqlalchemy < 3.0.0 automatically enabled
# SQLALCHEMY_RECORD_QUERIES in debug or test mode
debug_enables_record_queries = True
location_property: str = "context"
else:
location_property = "location"
sqlalchemy_available = True
def query_signer() -> itsdangerous.URLSafeSerializer:
return itsdangerous.URLSafeSerializer(
current_app.config["SECRET_KEY"], salt="fdt-sql-query"
)
def is_select(statement: str | bytes) -> bool:
statement = statement.lower().strip()
if isinstance(statement, bytes):
return statement.startswith(b"select")
return statement.startswith("select") # pyright: ignore
def dump_query(statement: str, params: t.Any) -> str | None:
if not params or not is_select(statement):
return None
try:
return query_signer().dumps([statement, params])
except TypeError:
return None
def load_query(data: str) -> tuple[str, t.Any]:
try:
statement, params = query_signer().loads(data)
except (itsdangerous.BadSignature, TypeError):
abort(406)
# Make sure it is a select statement
if not is_select(statement):
abort(406)
return statement, params
def extension_used() -> bool:
return "sqlalchemy" in current_app.extensions
def recording_enabled() -> bool:
return (
debug_enables_record_queries and current_app.debug
) or current_app.config.get("SQLALCHEMY_RECORD_QUERIES", False)
def is_available() -> bool:
return sqlalchemy_available and extension_used() and recording_enabled()
def get_queries() -> list[t.Any]:
if get_recorded_queries:
return get_recorded_queries()
else:
return []
class SQLAlchemyDebugPanel(DebugPanel):
"""Panel that displays the time a response took in milliseconds."""
name = "SQLAlchemy"
@property
def has_content(self) -> bool: # type: ignore[override]
return bool(get_queries()) or not is_available()
def nav_title(self) -> str:
return "SQLAlchemy"
def nav_subtitle(self) -> str:
count = len(get_queries())
if not count and not is_available():
return "Unavailable"
plural = "query" if count == 1 else "queries"
return f"{count} {plural}"
def title(self) -> str:
return "SQLAlchemy queries"
def url(self) -> str:
return ""
def content(self) -> str:
queries = get_queries()
if not queries and not is_available():
return self.render(
"panels/sqlalchemy_error.html",
{
"sqlalchemy_available": sqlalchemy_available,
"extension_used": extension_used(),
"recording_enabled": recording_enabled(),
},
)
data = []
for query in queries:
data.append(
{
"duration": query.duration,
"sql": format_sql(query.statement, query.parameters),
"signed_query": dump_query(query.statement, query.parameters),
"location_long": getattr(query, location_property),
"location": format_fname(getattr(query, location_property)),
}
)
return self.render("panels/sqlalchemy.html", {"queries": data})
# Panel views
@module.route("/sqlalchemy/sql_select", methods=["GET", "POST"])
@module.route(
"/sqlalchemy/sql_explain", methods=["GET", "POST"], defaults=dict(explain=True)
)
def sql_select(explain: bool = False) -> str:
statement, params = load_query(request.args["query"])
engine = current_app.extensions["sqlalchemy"].engine
if explain:
if engine.driver == "pysqlite":
statement = f"EXPLAIN QUERY PLAN\n{statement}"
else:
statement = f"EXPLAIN\n{statement}"
result = engine.execute(statement, params)
return g.debug_toolbar.render( # type: ignore[no-any-return]
"panels/sqlalchemy_select.html",
{
"result": result.fetchall(),
"headers": result.keys(),
"sql": format_sql(statement, params),
"duration": float(request.args["duration"]),
},
)

View File

@@ -0,0 +1,149 @@
from __future__ import annotations
import json
import sys
import typing as t
import uuid
from collections import deque
from flask import abort
from flask import current_app
from flask import g
from flask import request
from flask import Response
from flask import template_rendered
from flask import url_for
from jinja2 import Template
from .. import module
from . import DebugPanel
class TemplateDebugPanel(DebugPanel):
"""Panel that displays the time a response took in milliseconds."""
name = "Template"
has_content = True
# save the context for the 5 most recent requests
template_cache: deque[tuple[str, list[dict[str, t.Any]]]] = deque(maxlen=5)
@classmethod
def get_cache_for_key(cls, key: str) -> list[dict[str, t.Any]]:
for cache_key, value in cls.template_cache:
if key == cache_key:
return value
raise KeyError(key)
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
super().__init__(*args, **kwargs)
self.key: str = str(uuid.uuid4())
self.templates: list[dict[str, t.Any]] = []
template_rendered.connect(self._store_template_info)
def _store_template_info(self, sender: t.Any, **kwargs: t.Any) -> None:
# only record in the cache if the editor is enabled and there is
# actually a template for this request
if not self.templates and is_editor_enabled():
self.template_cache.append((self.key, self.templates))
self.templates.append(kwargs)
def nav_title(self) -> str:
return "Templates"
def nav_subtitle(self) -> str:
return f"{len(self.templates)} rendered"
def title(self) -> str:
return "Templates"
def url(self) -> str:
return ""
def content(self) -> str:
return self.render(
"panels/template.html",
{
"key": self.key,
"templates": self.templates,
"editable": is_editor_enabled(),
},
)
def is_editor_enabled() -> bool:
return current_app.config.get("DEBUG_TB_TEMPLATE_EDITOR_ENABLED", False) # type: ignore
def require_enabled() -> None:
if not is_editor_enabled():
abort(403)
def _get_source(template: Template) -> str:
if template.filename is None:
return ""
with open(template.filename, "rb") as fp:
source = fp.read()
return source.decode(_template_encoding())
def _template_encoding() -> str:
return getattr(current_app.jinja_loader, "encoding", "utf-8")
@module.route("/template/<key>")
def template_editor(key: str) -> str:
require_enabled()
# TODO set up special loader that caches templates it loads
# and can override template contents
templates = [t["template"] for t in TemplateDebugPanel.get_cache_for_key(key)]
return g.debug_toolbar.render( # type: ignore[no-any-return]
"panels/template_editor.html",
{
"static_path": url_for("_debug_toolbar.static", filename=""),
"request": request,
"templates": [
{"name": t.name, "source": _get_source(t)} for t in templates
],
},
)
@module.route("/template/<key>/save", methods=["POST"])
def save_template(key: str) -> str:
require_enabled()
template = TemplateDebugPanel.get_cache_for_key(key)[0]["template"]
content = request.form["content"].encode(_template_encoding())
with open(template.filename, "wb") as fp:
fp.write(content)
return "ok"
@module.route("/template/<key>", methods=["POST"])
def template_preview(key: str) -> str | Response:
require_enabled()
context = TemplateDebugPanel.get_cache_for_key(key)[0]["context"]
content = request.form["content"]
env = current_app.jinja_env.overlay(autoescape=True)
try:
template = env.from_string(content)
return template.render(context)
except Exception as e:
tb = sys.exc_info()[2]
try:
while tb.tb_next: # type: ignore[union-attr]
tb = tb.tb_next # type: ignore[union-attr]
msg = {"lineno": tb.tb_lineno, "error": str(e)} # type: ignore[union-attr]
return Response(json.dumps(msg), status=400, mimetype="application/json")
finally:
del tb

View File

@@ -0,0 +1,93 @@
from __future__ import annotations
import time
from werkzeug import Request
from werkzeug import Response
from . import DebugPanel
try:
import resource
HAVE_RESOURCE = True
except ImportError:
HAVE_RESOURCE = False
class TimerDebugPanel(DebugPanel):
"""Panel that displays the time a response took in milliseconds."""
name = "Timer"
has_content = HAVE_RESOURCE
def process_request(self, request: Request) -> None:
self._start_time = time.time()
if HAVE_RESOURCE:
self._start_rusage = resource.getrusage(resource.RUSAGE_SELF)
def process_response(self, request: Request, response: Response) -> None:
self.total_time: float = (time.time() - self._start_time) * 1000
if HAVE_RESOURCE:
self._end_rusage = resource.getrusage(resource.RUSAGE_SELF)
def nav_title(self) -> str:
return "Time"
def nav_subtitle(self) -> str:
if not HAVE_RESOURCE:
return f"TOTAL: {self.total_time:0.2f}ms"
utime = self._end_rusage.ru_utime - self._start_rusage.ru_utime
stime = self._end_rusage.ru_stime - self._start_rusage.ru_stime
return f"CPU: {(utime + stime) * 1000.0:0.2f}ms ({self.total_time:0.2f}ms)"
def title(self) -> str:
return "Resource Usage"
def url(self) -> str:
return ""
def _elapsed_ru(self, name: str) -> float:
return getattr(self._end_rusage, name) - getattr(self._start_rusage, name) # type: ignore[no-any-return]
def content(self) -> str:
utime = 1000 * self._elapsed_ru("ru_utime")
stime = 1000 * self._elapsed_ru("ru_stime")
vcsw = self._elapsed_ru("ru_nvcsw")
ivcsw = self._elapsed_ru("ru_nivcsw")
# minflt = self._elapsed_ru("ru_minflt")
# majflt = self._elapsed_ru("ru_majflt")
# these are documented as not meaningful under Linux. If you're running BSD
# feel free to enable them, and add any others that I hadn't gotten to before
# I noticed that I was getting nothing but zeroes and that the docs agreed. :-(
# blkin = self._elapsed_ru("ru_inblock")
# blkout = self._elapsed_ru("ru_oublock")
# swap = self._elapsed_ru("ru_nswap")
# rss = self._end_rusage.ru_maxrss
# srss = self._end_rusage.ru_ixrss
# urss = self._end_rusage.ru_idrss
# usrss = self._end_rusage.ru_isrss
rows = (
("User CPU time", f"{utime:0.3f} msec"),
("System CPU time", f"{stime:0.3f} msec"),
("Total CPU time", f"{(utime + stime):0.3f} msec"),
("Elapsed time", f"{self.total_time:0.3f} msec"),
("Context switches", f"{vcsw} voluntary, {ivcsw} involuntary"),
# (
# "Memory use",
# f"{rss} max RSS, {srss} shared, {urss + usrss} unshared",
# ),
# ("Page faults", f"{minflt} no i/o, {majflt} requiring i/o"),
# ("Disk operations", f"{blkin} in, {blkout} out, {swap} swapout"),
)
context = self.context.copy()
context.update(
{
"rows": rows,
}
)
return self.render("panels/timer.html", context)

View File

@@ -0,0 +1,39 @@
from __future__ import annotations
import importlib.metadata
import os
from sysconfig import get_path
from . import DebugPanel
flask_version: str = importlib.metadata.version("flask")
class VersionDebugPanel(DebugPanel):
"""Panel that displays the Flask version."""
name = "Version"
has_content = True
def nav_title(self) -> str:
return "Versions"
def nav_subtitle(self) -> str:
return f"Flask {flask_version}"
def url(self) -> str:
return ""
def title(self) -> str:
return "Versions"
def content(self) -> str:
packages_metadata = [p.metadata for p in importlib.metadata.distributions()]
packages = sorted(packages_metadata, key=lambda p: p["Name"].lower())
return self.render(
"panels/versions.html",
{
"packages": packages,
"python_lib_dir": os.path.normpath(get_path("platlib")),
},
)

Some files were not shown because too many files have changed in this diff Show More