diff --git a/.eslintrc.json b/.eslintrc.js similarity index 83% rename from .eslintrc.json rename to .eslintrc.js index 8a2452b7a..b0c799d88 100644 --- a/.eslintrc.json +++ b/.eslintrc.js @@ -1,7 +1,9 @@ -{ +module.exports = { + root: true, "env": { "browser": true, - "es6": true + "es6": true, + node: true, }, "extends": "eslint:recommended", "parserOptions": { @@ -17,4 +19,4 @@ "prefer-const": "error", "semi": "error" } -} +}; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c3cb00937..2059d37f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.8 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9dece68ef..cb28e217e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] services: mariadb: @@ -30,12 +30,13 @@ jobs: - 3306:3306 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Get pip cache dir id: pip-cache @@ -77,14 +78,16 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] database: [postgresql, postgis] - # Add psycopg3 to our matrix for 3.10 and 3.11 + # Add psycopg3 to our matrix for modern python versions include: - python-version: '3.10' database: psycopg3 - python-version: '3.11' database: psycopg3 + - python-version: '3.12' + database: psycopg3 services: postgres: @@ -102,12 +105,13 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Get pip cache dir id: pip-cache @@ -152,15 +156,16 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Get pip cache dir id: pip-cache @@ -198,7 +203,7 @@ jobs: runs-on: "ubuntu-latest" needs: [sqlite, mysql, postgres] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: # Use latest, so it understands all syntax. @@ -230,7 +235,7 @@ jobs: fail-fast: false steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 001b07e34..c2f93ac73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: check-toml - id: check-yaml @@ -14,7 +14,7 @@ repos: hooks: - id: doc8 - repo: https://github.com/adamchainz/django-upgrade - rev: 1.14.0 + rev: 1.15.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] @@ -24,14 +24,15 @@ repos: - id: rst-backticks - id: rst-directive-colons - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.1 + rev: v4.0.0-alpha.8 hooks: - id: prettier + entry: env PRETTIER_LEGACY_CLI=1 prettier types_or: [javascript, css] args: - - --trailing-comma=es5 + - --trailing-comma=es5 - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.46.0 + rev: v8.56.0 hooks: - id: eslint files: \.js?$ @@ -39,21 +40,16 @@ repos: args: - --fix - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.0.282' + rev: 'v0.1.11' hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] -- repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - language_version: python3 - entry: black --target-version=py38 + - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt - rev: 0.13.0 + rev: 1.5.3 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.13 + rev: v0.15 hooks: - id: validate-pyproject diff --git a/README.rst b/README.rst index b10a9ad91..0eaaa6bd3 100644 --- a/README.rst +++ b/README.rst @@ -47,6 +47,9 @@ contributed by the community. The current stable version of the Debug Toolbar is 4.1.0. It works on Django ≥ 3.2.4. +The Debug Toolbar does not currently support `Django's asynchronous views +`_. + Documentation, including installation and configuration instructions, is available at https://django-debug-toolbar.readthedocs.io/. diff --git a/debug_toolbar/__init__.py b/debug_toolbar/__init__.py index dbe08451f..9a8c2b24f 100644 --- a/debug_toolbar/__init__.py +++ b/debug_toolbar/__init__.py @@ -4,7 +4,7 @@ # Do not use pkg_resources to find the version but set it here directly! # see issue #1446 -VERSION = "4.2.0" +VERSION = "4.3.0" # Code that discovers files or modules in INSTALLED_APPS imports this module. urls = "debug_toolbar.urls", APP_NAME diff --git a/debug_toolbar/panels/profiling.py b/debug_toolbar/panels/profiling.py index 9d10229ad..2b7742400 100644 --- a/debug_toolbar/panels/profiling.py +++ b/debug_toolbar/panels/profiling.py @@ -42,10 +42,14 @@ def is_project_func(self): """ if hasattr(settings, "BASE_DIR"): file_name, _, _ = self.func - return ( - str(settings.BASE_DIR) in file_name - and "/site-packages/" not in file_name - and "/dist-packages/" not in file_name + base_dir = str(settings.BASE_DIR) + + file_name = os.path.normpath(file_name) + base_dir = os.path.normpath(base_dir) + + return file_name.startswith(base_dir) and not any( + directory in file_name.split(os.path.sep) + for directory in ["site-packages", "dist-packages"] ) return None diff --git a/debug_toolbar/panels/sql/tracking.py b/debug_toolbar/panels/sql/tracking.py index 0c53dc2c5..b5fc81234 100644 --- a/debug_toolbar/panels/sql/tracking.py +++ b/debug_toolbar/panels/sql/tracking.py @@ -109,21 +109,6 @@ class NormalCursorMixin(DjDTCursorWrapperMixin): Wraps a cursor and logs queries. """ - def _quote_expr(self, element): - if isinstance(element, str): - return "'%s'" % element.replace("'", "''") - else: - return repr(element) - - def _quote_params(self, params): - if not params: - return params - if isinstance(params, dict): - return {key: self._quote_expr(value) for key, value in params.items()} - if isinstance(params, tuple): - return tuple(self._quote_expr(p) for p in params) - return [self._quote_expr(p) for p in params] - def _decode(self, param): if PostgresJson and isinstance(param, PostgresJson): # psycopg3 @@ -157,9 +142,7 @@ def _last_executed_query(self, sql, params): # process during the .last_executed_query() call. self.db._djdt_logger = None try: - return self.db.ops.last_executed_query( - self.cursor, sql, self._quote_params(params) - ) + return self.db.ops.last_executed_query(self.cursor, sql, params) finally: self.db._djdt_logger = self.logger diff --git a/debug_toolbar/panels/templates/panel.py b/debug_toolbar/panels/templates/panel.py index 72565f016..f8c9242ca 100644 --- a/debug_toolbar/panels/templates/panel.py +++ b/debug_toolbar/panels/templates/panel.py @@ -83,58 +83,11 @@ def _store_template_info(self, sender, **kwargs): if is_debug_toolbar_template: return - context_list = [] - for context_layer in context.dicts: - if hasattr(context_layer, "items") and context_layer: - # Check if the layer is in the cache. - pformatted = None - for key_values, _pformatted in self.pformat_layers: - if key_values == context_layer: - pformatted = _pformatted - break - - if pformatted is None: - temp_layer = {} - for key, value in context_layer.items(): - # Replace any request elements - they have a large - # Unicode representation and the request data is - # already made available from the Request panel. - if isinstance(value, http.HttpRequest): - temp_layer[key] = "<>" - # Replace the debugging sql_queries element. The SQL - # data is already made available from the SQL panel. - elif key == "sql_queries" and isinstance(value, list): - temp_layer[key] = "<>" - # Replace LANGUAGES, which is available in i18n context - # processor - elif key == "LANGUAGES" and isinstance(value, tuple): - temp_layer[key] = "<>" - # QuerySet would trigger the database: user can run the - # query from SQL Panel - elif isinstance(value, (QuerySet, RawQuerySet)): - temp_layer[key] = "<<{} of {}>>".format( - value.__class__.__name__.lower(), - value.model._meta.label, - ) - else: - token = allow_sql.set(False) # noqa: FBT003 - try: - saferepr(value) # this MAY trigger a db query - except SQLQueryTriggered: - temp_layer[key] = "<>" - except UnicodeEncodeError: - temp_layer[key] = "<>" - except Exception: - temp_layer[key] = "<>" - else: - temp_layer[key] = value - finally: - allow_sql.reset(token) - pformatted = pformat(temp_layer) - self.pformat_layers.append((context_layer, pformatted)) - context_list.append(pformatted) - - kwargs["context"] = context_list + kwargs["context"] = [ + context_layer + for context_layer in context.dicts + if hasattr(context_layer, "items") and context_layer + ] kwargs["context_processors"] = getattr(context, "context_processors", None) self.templates.append(kwargs) @@ -167,6 +120,63 @@ def enable_instrumentation(self): def disable_instrumentation(self): template_rendered.disconnect(self._store_template_info) + def process_context_list(self, context_layers): + context_list = [] + for context_layer in context_layers: + # Check if the layer is in the cache. + pformatted = None + for key_values, _pformatted in self.pformat_layers: + if key_values == context_layer: + pformatted = _pformatted + break + + if pformatted is None: + temp_layer = {} + for key, value in context_layer.items(): + # Do not force evaluating LazyObject + if hasattr(value, "_wrapped"): + # SimpleLazyObject has __repr__ which includes actual value + # if it has been already evaluated + temp_layer[key] = repr(value) + # Replace any request elements - they have a large + # Unicode representation and the request data is + # already made available from the Request panel. + elif isinstance(value, http.HttpRequest): + temp_layer[key] = "<>" + # Replace the debugging sql_queries element. The SQL + # data is already made available from the SQL panel. + elif key == "sql_queries" and isinstance(value, list): + temp_layer[key] = "<>" + # Replace LANGUAGES, which is available in i18n context + # processor + elif key == "LANGUAGES" and isinstance(value, tuple): + temp_layer[key] = "<>" + # QuerySet would trigger the database: user can run the + # query from SQL Panel + elif isinstance(value, (QuerySet, RawQuerySet)): + temp_layer[ + key + ] = f"<<{value.__class__.__name__.lower()} of {value.model._meta.label}>>" + else: + token = allow_sql.set(False) # noqa: FBT003 + try: + saferepr(value) # this MAY trigger a db query + except SQLQueryTriggered: + temp_layer[key] = "<>" + except UnicodeEncodeError: + temp_layer[key] = "<>" + except Exception: + temp_layer[key] = "<>" + else: + temp_layer[key] = value + finally: + allow_sql.reset(token) + pformatted = pformat(temp_layer) + self.pformat_layers.append((context_layer, pformatted)) + context_list.append(pformatted) + + return context_list + def generate_stats(self, request, response): template_context = [] for template_data in self.templates: @@ -182,8 +192,11 @@ def generate_stats(self, request, response): info["template"] = template # Clean up context for better readability if self.toolbar.config["SHOW_TEMPLATE_CONTEXT"]: - context_list = template_data.get("context", []) - info["context"] = "\n".join(context_list) + if "context_list" not in template_data: + template_data["context_list"] = self.process_context_list( + template_data.get("context", []) + ) + info["context"] = "\n".join(template_data["context_list"]) template_context.append(info) # Fetch context_processors/template_dirs from any template diff --git a/debug_toolbar/settings.py b/debug_toolbar/settings.py index eb6b59209..1df24527d 100644 --- a/debug_toolbar/settings.py +++ b/debug_toolbar/settings.py @@ -42,6 +42,7 @@ "SQL_WARNING_THRESHOLD": 500, # milliseconds "OBSERVE_REQUEST_CALLBACK": "debug_toolbar.toolbar.observe_request", "TOOLBAR_LANGUAGE": None, + "UPDATE_ON_FETCH": False, } diff --git a/debug_toolbar/static/debug_toolbar/js/history.js b/debug_toolbar/static/debug_toolbar/js/history.js index b30fcabae..314ddb3ef 100644 --- a/debug_toolbar/static/debug_toolbar/js/history.js +++ b/debug_toolbar/static/debug_toolbar/js/history.js @@ -104,3 +104,6 @@ $$.on(djDebug, "click", ".refreshHistory", function (event) { event.preventDefault(); refreshHistory(); }); +// We don't refresh the whole toolbar each fetch or ajax request, +// so we need to refresh the history when we open the panel +$$.onPanelRender(djDebug, "HistoryPanel", refreshHistory); diff --git a/debug_toolbar/static/debug_toolbar/js/toolbar.js b/debug_toolbar/static/debug_toolbar/js/toolbar.js index 9546ef27e..199616336 100644 --- a/debug_toolbar/static/debug_toolbar/js/toolbar.js +++ b/debug_toolbar/static/debug_toolbar/js/toolbar.js @@ -17,8 +17,10 @@ function getDebugElement() { const djdt = { handleDragged: false, + needUpdateOnFetch: false, init() { const djDebug = getDebugElement(); + djdt.needUpdateOnFetch = djDebug.dataset.updateOnFetch === "True"; $$.on(djDebug, "click", "#djDebugPanelList li a", function (event) { event.preventDefault(); if (!this.className) { @@ -226,7 +228,7 @@ const djdt = { const handle = document.getElementById("djDebugToolbarHandle"); // set handle position const handleTop = Math.min( - localStorage.getItem("djdt.top") || 0, + localStorage.getItem("djdt.top") || 265, window.innerHeight - handle.offsetWidth ); handle.style.top = handleTop + "px"; @@ -274,7 +276,9 @@ const djdt = { storeId = encodeURIComponent(storeId); const dest = `${sidebarUrl}?store_id=${storeId}`; slowjax(dest).then(function (data) { - replaceToolbarState(storeId, data); + if (djdt.needUpdateOnFetch){ + replaceToolbarState(storeId, data); + } }); } diff --git a/debug_toolbar/templates/debug_toolbar/base.html b/debug_toolbar/templates/debug_toolbar/base.html index 5447970af..6f4967f21 100644 --- a/debug_toolbar/templates/debug_toolbar/base.html +++ b/debug_toolbar/templates/debug_toolbar/base.html @@ -16,7 +16,7 @@ data-sidebar-url="{{ history_url }}" {% endif %} data-default-show="{% if toolbar.config.SHOW_COLLAPSED %}false{% else %}true{% endif %}" - {{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }}> + {{ toolbar.config.ROOT_TAG_EXTRA_ATTRS|safe }} data-update-on-fetch="{{ toolbar.config.UPDATE_ON_FETCH }}">
  • {% trans "Hide" %} »
  • diff --git a/debug_toolbar/utils.py b/debug_toolbar/utils.py index 7234f1f77..3a9d0882e 100644 --- a/debug_toolbar/utils.py +++ b/debug_toolbar/utils.py @@ -162,13 +162,15 @@ def get_template_source_from_exception_info( def get_name_from_obj(obj: Any) -> str: - name = obj.__name__ if hasattr(obj, "__name__") else obj.__class__.__name__ - - if hasattr(obj, "__module__"): - module = obj.__module__ - name = f"{module}.{name}" - - return name + """Get the best name as `str` from a view or a object.""" + # This is essentially a rewrite of the `django.contrib.admindocs.utils.get_view_name` + # https://github.com/django/django/blob/9a22d1769b042a88741f0ff3087f10d94f325d86/django/contrib/admindocs/utils.py#L26-L32 + if hasattr(obj, "view_class"): + klass = obj.view_class + return f"{klass.__module__}.{klass.__qualname__}" + mod_name = obj.__module__ + view_name = getattr(obj, "__qualname__", obj.__class__.__name__) + return mod_name + "." + view_name def getframeinfo(frame: Any, context: int = 1) -> inspect.Traceback: @@ -211,7 +213,7 @@ def getframeinfo(frame: Any, context: int = 1) -> inspect.Traceback: def get_sorted_request_variable( - variable: Union[Dict[str, Any], QueryDict] + variable: Union[Dict[str, Any], QueryDict], ) -> Dict[str, Union[List[Tuple[str, Any]], Any]]: """ Get a data structure for showing a sorted list of variables from the diff --git a/docs/changes.rst b/docs/changes.rst index 89f5bdc7e..e2a610991 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -4,6 +4,28 @@ Change log Pending ------- +4.3.0 (2024-02-01) +------------------ + +* Dropped support for Django 4.0. +* Added Python 3.12 to test matrix. +* Removed outdated third-party panels from the list. +* Avoided the unnecessary work of recursively quoting SQL parameters. +* Postponed context process in templates panel to include lazy evaluated + content. +* Fixed template panel to avoid evaluating ``LazyObject`` when not already + evaluated. +* Added support for Django 5.0. +* Refactor the ``utils.get_name_from_obj`` to simulate the behavior of + ``django.contrib.admindocs.utils.get_view_name``. +* Switched from black to the `ruff formatter + `__. +* Changed the default position of the toolbar from top to the upper top + position. +* Added the setting, ``UPDATE_ON_FETCH`` to control whether the + toolbar automatically updates to the latest AJAX request or not. + It defaults to ``False``. + 4.2.0 (2023-08-10) ------------------ diff --git a/docs/conf.py b/docs/conf.py index 7fa8e6fce..f87b8f19a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ copyright = copyright.format(datetime.date.today().year) # The full version, including alpha/beta/rc tags -release = "4.2.0" +release = "4.3.0" # -- General configuration --------------------------------------------------- diff --git a/docs/configuration.rst b/docs/configuration.rst index 887608c6e..8271092ca 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -163,6 +163,16 @@ Toolbar options but want to render your application in French, you would set this to ``"en-us"`` and :setting:`LANGUAGE_CODE` to ``"fr"``. +.. _UPDATE_ON_FETCH: + +* ``UPDATE_ON_FETCH`` + + Default: ``False`` + + This controls whether the toolbar should update to the latest AJAX + request when it occurs. This is especially useful when using htmx + boosting or similar JavaScript techniques. + Panel options ~~~~~~~~~~~~~ diff --git a/docs/installation.rst b/docs/installation.rst index 3b65ff8e2..a350d9c3a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -7,6 +7,10 @@ Process Each of the following steps needs to be configured for the Debug Toolbar to be fully functional. +.. warning:: + + The Debug Toolbar does not currently support `Django's asynchronous views `_. + 1. Install the Package ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/panels.rst b/docs/panels.rst index 61a23ce61..db4e9311f 100644 --- a/docs/panels.rst +++ b/docs/panels.rst @@ -152,27 +152,6 @@ using Brendan Gregg's `flamegraph.pl script `_ to perform the heavy lifting. -HTML Tidy/Validator -~~~~~~~~~~~~~~~~~~~ - -URL: https://github.com/joymax/django-dtpanel-htmltidy - -Path: ``debug_toolbar_htmltidy.panels.HTMLTidyDebugPanel`` - -HTML Tidy or HTML Validator is a custom panel that validates your HTML and -displays warnings and errors. - -Inspector -~~~~~~~~~ - -URL: https://github.com/santiagobasulto/debug-inspector-panel - -Path: ``inspector_panel.panels.inspector.InspectorPanel`` - -Retrieves and displays information you specify using the ``debug`` statement. -Inspector panel also logs to the console by default, but may be instructed not -to. - LDAP Tracing ~~~~~~~~~~~~ @@ -276,18 +255,6 @@ Path: ``requests_panel.panel.RequestsDebugPanel`` Lists HTTP requests made with the popular `requests `_ library. -Sites -~~~~~ - -URL: https://github.com/elvard/django-sites-toolbar - -Path: ``sites_toolbar.panels.SitesDebugPanel`` - -Browse Sites registered in ``django.contrib.sites`` and switch between them. -Useful to debug project when you use `django-dynamicsites -`_ which sets SITE_ID -dynamically. - Template Profiler ~~~~~~~~~~~~~~~~~ @@ -308,15 +275,6 @@ Path: ``template_timings_panel.panels.TemplateTimings.TemplateTimings`` Displays template rendering times for your Django application. -User -~~~~ - -URL: https://github.com/playfire/django-debug-toolbar-user-panel - -Path: ``debug_toolbar_user_panel.panels.UserPanel`` - -Easily switch between logged in users, see properties of current user. - VCS Info ~~~~~~~~ diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 7a15d9aeb..436977bdc 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -6,6 +6,7 @@ Pympler Roboto Transifex Werkzeug +ajax async backend backends diff --git a/pyproject.toml b/pyproject.toml index 637dada5e..e529808cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,9 +18,9 @@ classifiers = [ "Environment :: Web Environment", "Framework :: Django", "Framework :: Django :: 3.2", - "Framework :: Django :: 4.0", "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", @@ -30,6 +30,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", ] dynamic = [ @@ -51,44 +52,28 @@ path = "debug_toolbar/__init__.py" [tool.ruff] extend-select = [ - # pyflakes, pycodestyle - "F", "E", "W", - # mmcabe - # "C90", - # isort - "I", - # pep8-naming - # "N", - # pyupgrade - "UP", - # flake8-2020 - # "YTT", - # flake8-boolean-trap - "FBT", - # flake8-bugbear - "B", - # flake8-comprehensions - "C4", - # flake8-django - "DJ", - # flake8-pie - "PIE", - # flake8-simplify - "SIM", - # flake8-gettext - "INT", - # pygrep-hooks - "PGH", - # pylint - # "PL", - # unused noqa - "RUF100", + "ASYNC", # flake8-async + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "C90", # McCabe cyclomatic complexity + "DJ", # flake8-django + "E", # pycodestyle errors + "F", # Pyflakes + "FBT", # flake8-boolean-trap + "I", # isort + "INT", # flake8-gettext + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "RUF100", # Unused noqa directive + "SIM", # flake8-simplify + "SLOT", # flake8-slots + "UP", # pyupgrade + "W", # pycodestyle warnings ] extend-ignore = [ - # Allow zip() without strict= - "B905", - # No line length errors - "E501", + "B905", # Allow zip() without strict= + "E501", # Ignore line length violations + "SIM108", # Use ternary operator instead of if-else-block ] fix = true show-fixes = true @@ -98,14 +83,12 @@ target-version = "py38" combine-as-imports = true [tool.ruff.mccabe] -max-complexity = 15 +max-complexity = 16 [tool.ruff.per-file-ignores] "*/migrat*/*" = [ - # Allow using PascalCase model names in migrations - "N806", - # Ignore the fact that migration files are invalid module names - "N999", + "N806", # Allow using PascalCase model names in migrations + "N999", # Ignore the fact that migration files are invalid module names ] [tool.coverage.html] diff --git a/tests/panels/test_profiling.py b/tests/panels/test_profiling.py index ff613dfe1..88ec57dd6 100644 --- a/tests/panels/test_profiling.py +++ b/tests/panels/test_profiling.py @@ -1,3 +1,6 @@ +import sys +import unittest + from django.contrib.auth.models import User from django.db import IntegrityError, transaction from django.http import HttpResponse @@ -50,6 +53,10 @@ def test_cum_time_threshold(self): self.assertNotIn("render", content) self.assertValidHTML(content) + @unittest.skipUnless( + sys.version_info < (3, 12, 0), + "Python 3.12 no longer contains a frame for list comprehensions.", + ) def test_listcomp_escaped(self): self._get_response = lambda request: listcomp_view(request) response = self.panel.process_request(self.request) diff --git a/tests/panels/test_template.py b/tests/panels/test_template.py index 37e70cfa5..eb23cde31 100644 --- a/tests/panels/test_template.py +++ b/tests/panels/test_template.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import User from django.template import Context, RequestContext, Template from django.test import override_settings +from django.utils.functional import SimpleLazyObject from ..base import BaseTestCase, IntegrationTestCase from ..forms import TemplateReprForm @@ -21,6 +22,7 @@ def tearDown(self): super().tearDown() def test_queryset_hook(self): + response = self.panel.process_request(self.request) t = Template("No context variables here!") c = Context( { @@ -29,12 +31,13 @@ def test_queryset_hook(self): } ) t.render(c) + self.panel.generate_stats(self.request, response) # ensure the query was NOT logged self.assertEqual(len(self.sql_panel._queries), 0) self.assertEqual( - self.panel.templates[0]["context"], + self.panel.templates[0]["context_list"], [ "{'False': False, 'None': None, 'True': True}", "{'deep_queryset': '<>',\n" @@ -99,16 +102,34 @@ def test_disabled(self): self.assertFalse(self.panel.enabled) def test_empty_context(self): + response = self.panel.process_request(self.request) t = Template("") c = Context({}) t.render(c) + self.panel.generate_stats(self.request, response) # Includes the builtin context but not the empty one. self.assertEqual( - self.panel.templates[0]["context"], + self.panel.templates[0]["context_list"], ["{'False': False, 'None': None, 'True': True}"], ) + def test_lazyobject(self): + response = self.panel.process_request(self.request) + t = Template("") + c = Context({"lazy": SimpleLazyObject(lambda: "lazy_value")}) + t.render(c) + self.panel.generate_stats(self.request, response) + self.assertNotIn("lazy_value", self.panel.content) + + def test_lazyobject_eval(self): + response = self.panel.process_request(self.request) + t = Template("{{lazy}}") + c = Context({"lazy": SimpleLazyObject(lambda: "lazy_value")}) + self.assertEqual(t.render(c), "lazy_value") + self.panel.generate_stats(self.request, response) + self.assertIn("lazy_value", self.panel.content) + @override_settings( DEBUG=True, DEBUG_TOOLBAR_PANELS=["debug_toolbar.panels.templates.TemplatesPanel"] diff --git a/tests/templates/ajax/ajax.html b/tests/templates/ajax/ajax.html new file mode 100644 index 000000000..c9de3acb6 --- /dev/null +++ b/tests/templates/ajax/ajax.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block content %} +
    click for ajax
    + + +{% endblock %} diff --git a/tests/test_integration.py b/tests/test_integration.py index b77b7cede..379fafaf4 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,5 +1,6 @@ import os import re +import time import unittest import html5lib @@ -749,3 +750,24 @@ def test_toolbar_language_will_render_to_locale_when_set_both(self): ) self.assertIn("Query", table.text) self.assertIn("Action", table.text) + + def test_ajax_dont_refresh(self): + self.get("/ajax/") + make_ajax = self.selenium.find_element(By.ID, "click_for_ajax") + make_ajax.click() + history_panel = self.selenium.find_element(By.ID, "djdt-HistoryPanel") + self.assertIn("/ajax/", history_panel.text) + self.assertNotIn("/json_view/", history_panel.text) + + @override_settings(DEBUG_TOOLBAR_CONFIG={"UPDATE_ON_FETCH": True}) + def test_ajax_refresh(self): + self.get("/ajax/") + make_ajax = self.selenium.find_element(By.ID, "click_for_ajax") + make_ajax.click() + # Need to wait until the ajax request is over and json_view is displayed on the toolbar + time.sleep(2) + history_panel = self.wait.until( + lambda selenium: self.selenium.find_element(By.ID, "djdt-HistoryPanel") + ) + self.assertNotIn("/ajax/", history_panel.text) + self.assertIn("/json_view/", history_panel.text) diff --git a/tests/test_utils.py b/tests/test_utils.py index f8c47502a..26bfce005 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,18 +18,24 @@ def x(): return 1 res = get_name_from_obj(x) - self.assertEqual(res, "tests.test_utils.x") + self.assertEqual( + res, "tests.test_utils.GetNameFromObjTestCase.test_func..x" + ) def test_lambda(self): res = get_name_from_obj(lambda: 1) - self.assertEqual(res, "tests.test_utils.") + self.assertEqual( + res, "tests.test_utils.GetNameFromObjTestCase.test_lambda.." + ) def test_class(self): class A: pass res = get_name_from_obj(A) - self.assertEqual(res, "tests.test_utils.A") + self.assertEqual( + res, "tests.test_utils.GetNameFromObjTestCase.test_class..A" + ) class RenderStacktraceTestCase(unittest.TestCase): diff --git a/tests/urls.py b/tests/urls.py index 6fc8811b7..f8929f1e8 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -21,6 +21,7 @@ path("cached_low_level_view/", views.cached_low_level_view), path("json_view/", views.json_view), path("redirect/", views.redirect_view), + path("ajax/", views.ajax_view), path("login_without_redirect/", LoginView.as_view(redirect_field_name=None)), path("admin/", admin.site.urls), path("__debug__/", include("debug_toolbar.urls")), diff --git a/tests/views.py b/tests/views.py index b2fd21c54..c7214029e 100644 --- a/tests/views.py +++ b/tests/views.py @@ -58,3 +58,7 @@ def listcomp_view(request): def redirect_view(request): return HttpResponseRedirect("/regular/redirect/") + + +def ajax_view(request): + return render(request, "ajax/ajax.html") diff --git a/tox.ini b/tox.ini index 5154d4907..254fb9b76 100644 --- a/tox.ini +++ b/tox.ini @@ -4,16 +4,15 @@ envlist = docs packaging py{38,39,310}-dj32-{sqlite,postgresql,postgis,mysql} - py310-dj40-sqlite py{310,311}-dj41-{sqlite,postgresql,postgis,mysql} - py{310,311}-dj{42,main}-{sqlite,postgresql,psycopg3,postgis,mysql} + py{310,311,312}-dj{42,50,main}-{sqlite,postgresql,psycopg3,postgis,mysql} [testenv] deps = dj32: django~=3.2.9 - dj40: django~=4.0.0 dj41: django~=4.1.3 dj42: django~=4.2.1 + dj50: django~=5.0a1 djmain: https://github.com/django/django/archive/main.tar.gz postgresql: psycopg2-binary psycopg3: psycopg[binary] @@ -49,25 +48,29 @@ allowlist_externals = make pip_pre = True commands = python -b -W always -m coverage run -m django test -v2 {posargs:tests} -[testenv:py{38,39,310,311}-dj{32,40,41,42,main}-{postgresql,psycopg3}] + +[testenv:py{38,39,310,311,312}-dj{32,41,42,50,main}-{postgresql,psycopg3}] setenv = {[testenv]setenv} DB_BACKEND = postgresql DB_PORT = {env:DB_PORT:5432} -[testenv:py{38,39,310,311}-dj{32,40,41,42,main}-postgis] + +[testenv:py{38,39,310,311,312}-dj{32,41,42,50,main}-postgis] setenv = {[testenv]setenv} DB_BACKEND = postgis DB_PORT = {env:DB_PORT:5432} -[testenv:py{38,39,310,311}-dj{32,40,41,42,main}-mysql] + +[testenv:py{38,39,310,311,312}-dj{32,41,42,50,main}-mysql] setenv = {[testenv]setenv} DB_BACKEND = mysql DB_PORT = {env:DB_PORT:3306} -[testenv:py{38,39,310,311}-dj{32,40,41,42,main}-sqlite] + +[testenv:py{38,39,310,311,312}-dj{32,41,42,50,main}-sqlite] setenv = {[testenv]setenv} DB_BACKEND = sqlite3 @@ -95,6 +98,7 @@ python = 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 [gh-actions:env] DB_BACKEND =