diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..95d8091 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,41 @@ +name: Tests +on: + push: + branches: + - master + paths-ignore: + - 'docs/**' + - '*.rst' + pull_request: + paths-ignore: + - 'docs/**' + - '*.rst' +jobs: + tests: + name: ${{ matrix.name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - {name: Linux, python: '3.12', os: ubuntu-latest, tox: py312} + - {name: Windows, python: '3.12', os: windows-latest, tox: py312} + - {name: Mac, python: '3.12', os: macos-latest, tox: py312} + - {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311} + - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} + - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} + - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} + - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} + - {name: '3.6', python: '3.6', os: ubuntu-20.04, tox: py36} # ubuntu-latest doesn't support 3.6 + - {name: Style, python: '3.10', os: ubuntu-latest, tox: stylecheck} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: update pip + run: | + pip install -U setuptools wheel + python -m pip install -U pip + - run: pip install tox + - run: tox -e ${{ matrix.tox }} diff --git a/.gitignore b/.gitignore index 3a389af..68bc17f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,160 @@ -*.egg-info -*.pyc +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python build/ +develop-eggs/ dist/ -docs/_build -.tox +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 043bbbc..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -sudo: false -language: python -python: "3.8" -env: - - TOXENV=py27 - - TOXENV=py38 -install: - - pip install tox -script: tox diff --git a/README.rst b/README.rst index ab44675..8cdec78 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ Flask Debug-toolbar This is a port of the excellent `django-debug-toolbar `_ for Flask applications. -.. image:: https://travis-ci.org/flask-debugtoolbar/flask-debugtoolbar.png?branch=master - :target: https://travis-ci.org/flask-debugtoolbar/flask-debugtoolbar +.. image:: https://github.com/pallets-eco/flask-debugtoolbar/actions/workflows/tests.yml/badge.svg + :target: https://github.com/pallets-eco/flask-debugtoolbar/actions Installation diff --git a/example/app.py b/example/app.py index 77c688e..cd35b9e 100644 --- a/example/app.py +++ b/example/app.py @@ -28,11 +28,6 @@ class ExampleModel(db.Model): value = db.Column(db.String(100), primary_key=True) -@app.before_first_request -def setup(): - db.create_all() - - @app.route('/') def index(): app.logger.info("Hello there") @@ -45,3 +40,7 @@ def redirect_example(): response = redirect(url_for('index')) response.set_cookie('test_cookie', '1') return response + + +with app.app_context(): + db.create_all() diff --git a/setup.py b/setup.py index 94054b0..6bf3d8b 100644 --- a/setup.py +++ b/setup.py @@ -8,5 +8,6 @@ 'Blinker', 'itsdangerous', 'werkzeug', + 'MarkupSafe', ], ) diff --git a/src/flask_debugtoolbar/__init__.py b/src/flask_debugtoolbar/__init__.py index 08185ea..3b3a7b7 100644 --- a/src/flask_debugtoolbar/__init__.py +++ b/src/flask_debugtoolbar/__init__.py @@ -1,4 +1,5 @@ import os +import urllib.parse import warnings import flask @@ -14,7 +15,6 @@ from jinja2 import __version__ as __jinja_version__ from jinja2 import Environment, PackageLoader -from werkzeug.urls import url_quote_plus from flask_debugtoolbar.compat import iteritems from flask_debugtoolbar.toolbar import DebugToolbar @@ -76,7 +76,7 @@ def __init__(self, app=None): autoescape=True, extensions=jinja_extensions, loader=PackageLoader(__name__, 'templates')) - self.jinja_env.filters['urlencode'] = url_quote_plus + self.jinja_env.filters['urlencode'] = urllib.parse.quote_plus self.jinja_env.filters['printable'] = _printable self.jinja_env.globals['url_for'] = url_for @@ -230,10 +230,11 @@ def process_response(self, response): response.headers['content-type'].startswith('text/html')): return response - if 'gzip' in response.headers.get('Content-Encoding', ''): - response_html = gzip_decompress(response.data).decode(response.charset) + 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.data.decode(response.charset) + response_html = response.get_data(as_text=True) no_case = response_html.lower() body_end = no_case.rfind('') @@ -257,8 +258,8 @@ def process_response(self, response): toolbar_html = toolbar.render_toolbar() content = ''.join((before, toolbar_html, after)) - content = content.encode(response.charset) - if 'gzip' in response.headers.get('Content-Encoding', ''): + content = content.encode('utf-8') + if content_encoding and 'gzip' in content_encoding: content = gzip_compress(content) response.response = [content] response.content_length = len(content) diff --git a/src/flask_debugtoolbar/panels/profiler.py b/src/flask_debugtoolbar/panels/profiler.py index 3294e4a..66f22aa 100644 --- a/src/flask_debugtoolbar/panels/profiler.py +++ b/src/flask_debugtoolbar/panels/profiler.py @@ -94,7 +94,7 @@ def process_response(self, request, response): def title(self): if not self.is_active: return "Profiler not active" - return 'View: %.2fms' % (float(self.stats.total_tt)*1000,) + return 'View: %.2fms' % (float(self.stats.total_tt) * 1000,) def nav_title(self): return 'Profiler' @@ -102,7 +102,7 @@ def nav_title(self): def nav_subtitle(self): if not self.is_active: return "in-active" - return 'View: %.2fms' % (float(self.stats.total_tt)*1000,) + return 'View: %.2fms' % (float(self.stats.total_tt) * 1000,) def url(self): return '' diff --git a/src/flask_debugtoolbar/panels/sqlalchemy.py b/src/flask_debugtoolbar/panels/sqlalchemy.py index 87f4bdd..13211eb 100644 --- a/src/flask_debugtoolbar/panels/sqlalchemy.py +++ b/src/flask_debugtoolbar/panels/sqlalchemy.py @@ -62,8 +62,7 @@ def extension_used(): def recording_enabled(): - return (current_app.debug - or current_app.config.get('SQLALCHEMY_RECORD_QUERIES')) + return (current_app.debug or current_app.config.get('SQLALCHEMY_RECORD_QUERIES')) def is_available(): diff --git a/src/flask_debugtoolbar/panels/timer.py b/src/flask_debugtoolbar/panels/timer.py index 2e04aa6..df3317e 100644 --- a/src/flask_debugtoolbar/panels/timer.py +++ b/src/flask_debugtoolbar/panels/timer.py @@ -52,8 +52,7 @@ def url(self): return '' def _elapsed_ru(self, name): - return (getattr(self._end_rusage, name) - - getattr(self._start_rusage, name)) + return (getattr(self._end_rusage, name) - getattr(self._start_rusage, name)) def content(self): diff --git a/src/flask_debugtoolbar/utils.py b/src/flask_debugtoolbar/utils.py index fc8ac70..76957e1 100644 --- a/src/flask_debugtoolbar/utils.py +++ b/src/flask_debugtoolbar/utils.py @@ -20,7 +20,9 @@ except ImportError: HAVE_SQLPARSE = False -from flask import current_app, Markup +from flask import current_app + +from markupsafe import Markup def format_fname(value): @@ -86,6 +88,7 @@ def format_sql(query, args): SqlLexer(), HtmlFormatter(noclasses=True, style=PYGMENT_STYLE))) + def gzip_compress(data, compresslevel=6): buff = io.BytesIO() with gzip.GzipFile(fileobj=buff, mode='wb', compresslevel=compresslevel) as f: diff --git a/test/basic_app.py b/test/basic_app.py index 6ec1ddd..9c3618d 100644 --- a/test/basic_app.py +++ b/test/basic_app.py @@ -4,10 +4,12 @@ from flask_debugtoolbar import DebugToolbarExtension app = Flask('basic_app') +app.config['DEBUG'] = True app.config['SECRET_KEY'] = 'abc123' app.config['SQLALCHEMY_RECORD_QUERIES'] = True -app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////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_DATABASE_URI'] = 'sqlite:///:memory:' +# This is no longer needed for Flask-SQLAlchemy 3.0+, +# if you're using 2.X you'll want to define this: # app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # make sure these are printable in the config panel @@ -23,12 +25,11 @@ class Foo(db.Model): id = db.Column(db.Integer, primary_key=True) -@app.before_first_request -def setup(): - db.create_all() - - @app.route('/') def index(): Foo.query.filter_by(id=1).all() return render_template('basic_app.html') + + +with app.app_context(): + db.create_all() diff --git a/test/test_utils.py b/test/test_utils.py index 22b29b3..4c4bea6 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -1,7 +1,8 @@ import ntpath import posixpath -from flask import Markup +from markupsafe import Markup + import pytest from flask_debugtoolbar.utils import (_relative_paths, _shortest_relative_path, diff --git a/tox.ini b/tox.ini index a4a8573..48be4c1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,9 @@ [tox] -envlist = py27,py36,py37,py38,stylecheck +envlist = + py27 + py3{12,11,10,9,8,7,6} + stylecheck +skip_missing_interpreters = True [testenv] deps = @@ -13,7 +17,9 @@ commands = deps = pycodestyle commands = - pycodestyle flask_debugtoolbar test + # E731: do not assign a lambda expression, use a def + # W504: line break after binary operator + pycodestyle src/flask_debugtoolbar test --ignore=E731,W504 [pycodestyle] max-line-length = 100