diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 147ae2af2a10131e80657e5fac2e9d39f2d045b6..649a2dfe0e2d8affd2c8cab443d95c422abe594f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: python-version: - - '3.12' + - '3.13' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -34,19 +34,20 @@ jobs: CELERY_BROKER_URL: redis://0.0.0.0:6379 DJANGO_SETTINGS_MODULE: config.settings.test_demo_app run: pytest --cov=./django_structlog_demo_project --cov-append django_structlog_demo_project - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false # disable for the moment because it prevents PR to succeed test: runs-on: ubuntu-latest strategy: matrix: python-version: - - '3.8' - '3.9' - '3.10' - '3.11' - '3.12' + - '3.13' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -58,14 +59,15 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install -U setuptools - python -m pip install tox tox-gh-actions + python -m pip install tox tox-gh-actions -r requirements/coverage.txt - name: Start Redis uses: supercharge/redis-github-action@1.8.0 - name: Test with tox run: tox - - uses: codecov/codecov-action@v4 + - uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false # disable for the moment because it prevents PR to succeed test-docs: needs: @@ -75,7 +77,7 @@ jobs: strategy: matrix: python-version: - - '3.12' + - '3.13' steps: - uses: actions/checkout@v4 - name: Install dependencies @@ -97,7 +99,7 @@ jobs: strategy: matrix: python-version: - - '3.12' + - '3.13' steps: - uses: actions/checkout@v4 - name: Install dependencies @@ -115,7 +117,7 @@ jobs: strategy: matrix: python-version: - - '3.12' + - '3.13' steps: - uses: actions/checkout@v4 - name: Install dependencies @@ -125,13 +127,41 @@ jobs: - name: run ruff run: ruff check . + mypy: + needs: + - test-demo-app + - test + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - '3.13' + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements/local.txt -r requirements/mypy.txt + - name: run mypy + run: mypy + + isort: + needs: + - test-demo-app + - test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: isort/isort-action@v1 + with: + requirements-files: "requirements/local-base.txt requirements/isort.txt" test-install-base: runs-on: ubuntu-latest strategy: matrix: python-version: - - '3.12' + - '3.13' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -157,7 +187,7 @@ jobs: strategy: matrix: python-version: - - '3.12' + - '3.13' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -188,7 +218,7 @@ jobs: strategy: matrix: python-version: - - '3.12' + - '3.13' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -220,6 +250,8 @@ jobs: - test-install-commands - black - ruff + - mypy + - isort runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 90f782cabfdb848223343a473a6e9d008d22260c..b3a2f7f1f3c2543f8d5c0e818584b66aa551e8a8 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -15,11 +15,11 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: 3.13 - uses: browniebroke/pre-commit-autoupdate-action@v1.0.0 - - uses: peter-evans/create-pull-request@v6.0.5 + - uses: peter-evans/create-pull-request@v7.0.6 with: token: ${{ secrets.GITHUB_TOKEN }} branch: update/pre-commit-hooks diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3b7a2d239cece09440076423787fd8d3564ad26e --- /dev/null +++ b/.gitignore @@ -0,0 +1,332 @@ +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +staticfiles/ + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# Environments +.venv +venv/ +ENV/ + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + + +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + + +### VisualStudioCode template +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + + +# Provided default Pycharm Run/Debug Configurations should be tracked by git +# In case of local modifications made by Pycharm, use update-index command +# for each changed file, like this: +# git update-index --assume-unchanged .idea/django_structlog_demo_project.iml +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/misc.xml +.idea/google-java-format.xml +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + + + +### Windows template +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + + +### macOS template +# General +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + +### SublimeText template +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# Workspace files are user-specific +*.sublime-workspace + +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project + +# SFTP configuration file +sftp-config.json + +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache + +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings + + +### Vim template +# Swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] + +# Session +Session.vim + +# Temporary +.netrwhist + +# Auto-generated tag files +tags + + +### Project template + +django_structlog_demo_project/media/ + +.pytest_cache/ + + +.ipython/ +.env +.envs/* +!.envs/.local/ +.vscode/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 285d2749204b92e1708f80c817e4cbb36b38a4fa..260ec7189e1cfc0896f3858fc3f0b747dbedfed7 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -5,11 +5,11 @@ <option name="ourVersions"> <value> <list size="5"> - <item index="0" class="java.lang.String" itemvalue="3.8" /> - <item index="1" class="java.lang.String" itemvalue="3.9" /> - <item index="2" class="java.lang.String" itemvalue="3.10" /> - <item index="3" class="java.lang.String" itemvalue="3.11" /> - <item index="4" class="java.lang.String" itemvalue="3.12" /> + <item index="0" class="java.lang.String" itemvalue="3.9" /> + <item index="1" class="java.lang.String" itemvalue="3.10" /> + <item index="2" class="java.lang.String" itemvalue="3.11" /> + <item index="3" class="java.lang.String" itemvalue="3.12" /> + <item index="4" class="java.lang.String" itemvalue="3.13" /> </list> </value> </option> diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 110dae9e3ca40bd249e911f7f7f86a71453bf0b0..d6a53e16530ad4e46a0cb261640e94ccadd8b4f7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,35 @@ repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.3 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort - repo: https://github.com/ambv/black - rev: 24.4.2 + rev: 24.10.0 hooks: - id: black language_version: python3 - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.4 + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.14.1 hooks: - - id: ruff - args: [--fix, --exit-non-zero-on-fix] + - id: mypy + args: [--no-incremental] + additional_dependencies: [ + celery-types==0.22.0, + "django-stubs[compatible-mypy]==5.1.2", + structlog==24.4.0, + django-extensions==3.2.3, + django-ipware==7.0.1, + ] + exclude: | + (?x)( + ^django_structlog_demo_project/| + ^config/| + ^docs/| + ^manage.py + ) + diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 29bad106ff60c42cc2e2fc167ae44c87e672e349..65be020c2b1a4d96fbd15a489e142d5dfc147ae7 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,9 +7,9 @@ version: 2 # Set the version of Python and other tools you might need build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.12" + python: "3.13" # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/README.rst b/README.rst index dfb425ec45a006ba665b505e6be9edc5a97d5a2c..b32e3b1b9900ea82026052d37f1806336baaf72b 100644 --- a/README.rst +++ b/README.rst @@ -5,8 +5,14 @@ django-structlog | |pypi| |wheels| |build-status| |docs| |coverage| |open_issues| |pull_requests| | |django| |python| |license| |black| |ruff| +| |django_packages| | |watchers| |stars| |forks| + +.. |django_packages| image:: https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26 + :target: https://djangopackages.org/packages/p/django-structlog/ + :alt: Published on Django Packages + .. |build-status| image:: https://github.com/jrobichaud/django-structlog/actions/workflows/main.yml/badge.svg?branch=main :target: https://github.com/jrobichaud/django-structlog/actions :alt: Build Status @@ -369,6 +375,44 @@ Json file (\ ``logs/json.log``\ ) Upgrade Guide ============= +.. _upgrade_9.0: + +Upgrading to 9.0+ +^^^^^^^^^^^^^^^^^ + +Minimum requirements +~~~~~~~~~~~~~~~~~~~~ +- requires python 3.9+ +- django 4.2 and 5.0+ are supported + + +Type hints +~~~~~~~~~~ + +``django-structlog`` now uses `python type hints <https://docs.python.org/3/library/typing.html>`_ and is being validated with `mypy <https://mypy.readthedocs.io/en/stable/>`_ ``--strict``. + + +For ``drf-standardized-errors`` users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now unhandled exceptions when using `drf-standardized-errors <https://github.com/ghazi-git/drf-standardized-errors>`_ will be intercepted and the exception logged properly. + +If you also use `structlog-sentry <https://github.com/kiwicom/structlog-sentry>`_, the exception will now be propagated as expected. + +Other libraries alike may be affected by this change. + +Internal changes in how ``RequestMiddleware`` handles exceptions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This only affects you if you implemented a middleware inheriting from ``RequestMiddleware`` and you overrided the ``process_exception`` method. + +Did you? + +If so: + + - ``RequestMiddleware.process_exception`` was renamed to ``RequestMiddleware._process_exception``, you should to the same in the middleware. + + .. _upgrade_8.0: Upgrading to 8.0+ diff --git a/compose/local/django/Dockerfile b/compose/local/django/Dockerfile index 62f0ec7fd00d682755e591ed8a6e238039ae2e5a..b7b6f8411f3bf78c6d64a3122000f2beeb2dedfe 100644 --- a/compose/local/django/Dockerfile +++ b/compose/local/django/Dockerfile @@ -10,9 +10,6 @@ ARG REQUIREMENTS_FILE=local.txt RUN <<EOF apk update - # psycopg2 dependencies - apk add --virtual build-deps gcc python3-dev musl-dev - apk add postgresql-dev # CFFI dependencies apk add libffi-dev py-cffi # https://docs.djangoproject.com/en/dev/ref/django-admin/#dbshell diff --git a/compose/local/django/entrypoint b/compose/local/django/entrypoint index 4f83411c46fc980af497e9b222c534bf4e89db2e..9255ee0fc1ec1e89cfd0ff2464a3eaac1aec9abd 100644 --- a/compose/local/django/entrypoint +++ b/compose/local/django/entrypoint @@ -20,17 +20,17 @@ postgres_ready() { python << END import sys -import psycopg2 +import psycopg try: - psycopg2.connect( + psycopg.connect( dbname="${POSTGRES_DB}", user="${POSTGRES_USER}", password="${POSTGRES_PASSWORD}", host="${POSTGRES_HOST}", port="${POSTGRES_PORT}", ) -except psycopg2.OperationalError: +except psycopg.OperationalError: sys.exit(-1) sys.exit(0) diff --git a/compose/local/docs/start b/compose/local/docs/start index fa672b616acc092de48699f6886cbca7a29db229..2e03361e60dc6d3b24fe37cd7ba6c0fa8f998c9a 100644 --- a/compose/local/docs/start +++ b/compose/local/docs/start @@ -7,7 +7,7 @@ set -o nounset # Basically we watch only README.rst, LICENCE.rst and everything under django_structlog sphinx-autobuild docs /docs/_build/html \ -b ${SPHINX_COMMAND} \ ---port 5000 \ +--port 8080 \ --host 0.0.0.0 \ --watch . \ --ignore "*___jb_*" \ diff --git a/config/settings/local.py b/config/settings/local.py index 0d9dcc6ebffc32549f686c6136e77f6f1b17edd5..aa84ba4d81e27926a95aabb5d6da2aa9847fa50c 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -1,7 +1,7 @@ import structlog from .base import * # noqa: F403 -from .base import env, MIDDLEWARE +from .base import MIDDLEWARE, env # GENERAL # ------------------------------------------------------------------------------ diff --git a/config/settings/test.py b/config/settings/test.py index 7d30c4d74e4311bf1f546dd960172444abadb6ff..18f288aa9293d031bf6b4c526d05b056828bc925 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -3,8 +3,8 @@ With these settings, tests run faster. """ import os -import environ +import environ import structlog env = environ.Env() diff --git a/config/urls.py b/config/urls.py index d5be8fc7590f2a8ee4e18ede7f170da8750fed7b..4eb77072f3fee5cdaab9c11dc2da352c6816c81f 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,12 +1,12 @@ from django.conf import settings from django.conf.urls import include -from django.urls import re_path from django.conf.urls.static import static from django.contrib import admin -from django.views.generic import TemplateView +from django.urls import re_path from django.views import defaults as default_views +from django.views.generic import TemplateView -from django_structlog_demo_project.home import views, api_views, ninja_views +from django_structlog_demo_project.home import api_views, ninja_views, views def uncaught_exception_view(request): diff --git a/django_structlog/__init__.py b/django_structlog/__init__.py index 0aa4ab95ca63156fbb08dd5168cd8340a2b9271c..8783fd21e83950502f209a38daf7c9bfbc95cdba 100644 --- a/django_structlog/__init__.py +++ b/django_structlog/__init__.py @@ -3,6 +3,6 @@ name = "django_structlog" -VERSION = (8, 1, 0) +VERSION = (9, 0, 1) __version__ = ".".join(str(v) for v in VERSION) diff --git a/django_structlog/app_settings.py b/django_structlog/app_settings.py index e6e91e7d68654a8b641ec430ee2b5e1793232098..ab560c82369770c72aa50796d39fee2eaffedfe2 100644 --- a/django_structlog/app_settings.py +++ b/django_structlog/app_settings.py @@ -8,19 +8,19 @@ class AppSettings: PREFIX = "DJANGO_STRUCTLOG_" @property - def CELERY_ENABLED(self): + def CELERY_ENABLED(self) -> bool: return getattr(settings, self.PREFIX + "CELERY_ENABLED", False) @property - def STATUS_4XX_LOG_LEVEL(self): + def STATUS_4XX_LOG_LEVEL(self) -> int: return getattr(settings, self.PREFIX + "STATUS_4XX_LOG_LEVEL", logging.WARNING) @property - def COMMAND_LOGGING_ENABLED(self): + def COMMAND_LOGGING_ENABLED(self) -> bool: return getattr(settings, self.PREFIX + "COMMAND_LOGGING_ENABLED", False) @property - def USER_ID_FIELD(self): + def USER_ID_FIELD(self) -> str: return getattr(settings, self.PREFIX + "USER_ID_FIELD", "pk") diff --git a/django_structlog/apps.py b/django_structlog/apps.py index 726205a686cf24355c6e7a026c5d648778f23c9d..4fdb3689b439572f323d72f6d46336459a12f8d8 100644 --- a/django_structlog/apps.py +++ b/django_structlog/apps.py @@ -6,7 +6,7 @@ from .app_settings import app_settings class DjangoStructLogConfig(AppConfig): name = "django_structlog" - def ready(self): + def ready(self) -> None: if app_settings.CELERY_ENABLED: from .celery.receivers import CeleryReceiver diff --git a/django_structlog/celery/receivers.py b/django_structlog/celery/receivers.py index cd2f64893e6486e7c3a85c25fab04fabcec8c03f..88201fca4430c86ace2cee38f2a3830a30cee0f0 100644 --- a/django_structlog/celery/receivers.py +++ b/django_structlog/celery/receivers.py @@ -1,36 +1,42 @@ +from typing import TYPE_CHECKING, Any, Optional, Type, cast + import structlog from celery import current_app from celery.signals import ( - before_task_publish, after_task_publish, + before_task_publish, + task_failure, task_prerun, + task_rejected, task_retry, - task_success, - task_failure, task_revoked, + task_success, task_unknown, - task_rejected, ) from . import signals +if TYPE_CHECKING: # pragma: no cover + from types import TracebackType logger = structlog.getLogger(__name__) class CeleryReceiver: - def __init__(self): + _priority: Optional[str] + + def __init__(self) -> None: self._priority = None def receiver_before_task_publish( self, - sender=None, - headers=None, - body=None, - properties=None, - routing_key=None, - **kwargs, - ): + sender: Optional[Type[Any]] = None, + headers: Optional[dict[str, Any]] = None, + body: Optional[dict[str, str]] = None, + properties: Optional[dict[str, Any]] = None, + routing_key: Optional[str] = None, + **kwargs: dict[str, str], + ) -> None: if current_app.conf.task_protocol < 2: return @@ -46,12 +52,16 @@ class CeleryReceiver: ) if properties: self._priority = properties.get("priority", None) - - headers["__django_structlog__"] = context + cast(dict[str, Any], headers)["__django_structlog__"] = context def receiver_after_task_publish( - self, sender=None, headers=None, body=None, routing_key=None, **kwargs - ): + self, + sender: Optional[Type[Any]] = None, + headers: Optional[dict[str, Optional[str]]] = None, + body: Optional[dict[str, Optional[str]]] = None, + routing_key: Optional[str] = None, + **kwargs: Any, + ) -> None: properties = {} if self._priority is not None: properties["priority"] = self._priority @@ -59,13 +69,23 @@ class CeleryReceiver: logger.info( "task_enqueued", - child_task_id=headers.get("id") if headers else body.get("id"), - child_task_name=headers.get("task") if headers else body.get("task"), + child_task_id=( + headers.get("id") + if headers + else cast(dict[str, Optional[str]], body).get("id") + ), + child_task_name=( + headers.get("task") + if headers + else cast(dict[str, Optional[str]], body).get("task") + ), routing_key=routing_key, **properties, ) - def receiver_task_prerun(self, task_id, task, *args, **kwargs): + def receiver_task_prerun( + self, task_id: str, task: Any, *args: Any, **kwargs: Any + ) -> None: structlog.contextvars.clear_contextvars() structlog.contextvars.bind_contextvars(task_id=task_id) metadata = getattr(task.request, "__django_structlog__", {}) @@ -75,10 +95,18 @@ class CeleryReceiver: ) logger.info("task_started", task=task.name) - def receiver_task_retry(self, request=None, reason=None, einfo=None, **kwargs): + def receiver_task_retry( + self, + request: Optional[Any] = None, + reason: Optional[str] = None, + einfo: Optional[Any] = None, + **kwargs: Any, + ) -> None: logger.warning("task_retrying", reason=reason) - def receiver_task_success(self, result=None, **kwargs): + def receiver_task_success( + self, result: Optional[str] = None, **kwargs: Any + ) -> None: signals.pre_task_succeeded.send( sender=self.receiver_task_success, logger=logger, result=result ) @@ -86,14 +114,14 @@ class CeleryReceiver: def receiver_task_failure( self, - task_id=None, - exception=None, - traceback=None, - einfo=None, - sender=None, - *args, - **kwargs, - ): + task_id: Optional[str] = None, + exception: Optional[Exception] = None, + traceback: Optional["TracebackType"] = None, + einfo: Optional[Any] = None, + sender: Optional[Type[Any]] = None, + *args: Any, + **kwargs: Any, + ) -> None: throws = getattr(sender, "throws", ()) if isinstance(exception, throws): logger.info( @@ -108,8 +136,13 @@ class CeleryReceiver: ) def receiver_task_revoked( - self, request=None, terminated=None, signum=None, expired=None, **kwargs - ): + self, + request: Any, + terminated: Optional[bool] = None, + signum: Optional[Any] = None, + expired: Optional[Any] = None, + **kwargs: Any, + ) -> None: metadata = getattr(request, "__django_structlog__", {}).copy() metadata["task_id"] = request.id metadata["task"] = request.task @@ -124,24 +157,31 @@ class CeleryReceiver: ) def receiver_task_unknown( - self, message=None, exc=None, name=None, id=None, **kwargs - ): + self, + message: Optional[str] = None, + exc: Optional[Exception] = None, + name: Optional[str] = None, + id: Optional[str] = None, + **kwargs: Any, + ) -> None: logger.error( "task_not_found", task=name, task_id=id, ) - def receiver_task_rejected(self, message=None, exc=None, **kwargs): + def receiver_task_rejected( + self, message: Any, exc: Optional[Exception] = None, **kwargs: Any + ) -> None: logger.exception( "task_rejected", task_id=message.properties.get("correlation_id") ) - def connect_signals(self): + def connect_signals(self) -> None: before_task_publish.connect(self.receiver_before_task_publish) after_task_publish.connect(self.receiver_after_task_publish) - def connect_worker_signals(self): + def connect_worker_signals(self) -> None: before_task_publish.connect(self.receiver_before_task_publish) after_task_publish.connect(self.receiver_after_task_publish) task_prerun.connect(self.receiver_task_prerun) diff --git a/django_structlog/celery/signals.py b/django_structlog/celery/signals.py index efa1b4532f88fe37c6f882c539d5cdac05920a64..f01b3291e56f2fa6a06dca60c2afed90b9328fea 100644 --- a/django_structlog/celery/signals.py +++ b/django_structlog/celery/signals.py @@ -1,6 +1,5 @@ import django.dispatch - bind_extra_task_metadata = django.dispatch.Signal() """ Signal to add extra ``structlog`` bindings from ``celery``'s task. diff --git a/django_structlog/celery/steps.py b/django_structlog/celery/steps.py index 9deb4a20c07055fa98e3b4c363f109c283fd2500..51b1a4e76e10d68e7a65c90a87e3a02cbe0c9458 100644 --- a/django_structlog/celery/steps.py +++ b/django_structlog/celery/steps.py @@ -1,3 +1,5 @@ +from typing import Any + from celery import bootsteps from .receivers import CeleryReceiver @@ -14,7 +16,7 @@ class DjangoStructLogInitStep(bootsteps.Step): """ - def __init__(self, parent, **kwargs): + def __init__(self, parent: Any, **kwargs: Any) -> None: super().__init__(parent, **kwargs) self.receiver = CeleryReceiver() self.receiver.connect_worker_signals() diff --git a/django_structlog/commands.py b/django_structlog/commands.py index 85b088c5bc27c1c32ad5725567ccf96924c19d4f..8ffca16a8ff9beeec186d9ac394e316608b645a2 100644 --- a/django_structlog/commands.py +++ b/django_structlog/commands.py @@ -1,16 +1,25 @@ -import structlog import uuid +from typing import TYPE_CHECKING, Any, List, Mapping, Tuple, Type + +import structlog +from django_extensions.management.signals import ( # type: ignore[import-untyped] + post_command, + pre_command, +) -from django_extensions.management.signals import pre_command, post_command +if TYPE_CHECKING: # pragma: no cover + import contextvars logger = structlog.getLogger(__name__) class DjangoCommandReceiver: - def __init__(self): + stack: List[Tuple[str, Mapping[str, "contextvars.Token[Any]"]]] + + def __init__(self) -> None: self.stack = [] - def pre_receiver(self, sender, *args, **kwargs): + def pre_receiver(self, sender: Type[Any], *args: Any, **kwargs: Any) -> None: command_id = str(uuid.uuid4()) if len(self.stack): parent_command_id, _ = self.stack[-1] @@ -26,13 +35,15 @@ class DjangoCommandReceiver: command_name=sender.__module__.replace(".management.commands", ""), ) - def post_receiver(self, sender, outcome, *args, **kwargs): + def post_receiver( + self, sender: Type[Any], outcome: str, *args: Any, **kwargs: Any + ) -> None: logger.info("command_finished") if len(self.stack): # pragma: no branch command_id, tokens = self.stack.pop() structlog.contextvars.reset_contextvars(**tokens) - def connect_signals(self): + def connect_signals(self) -> None: pre_command.connect(self.pre_receiver) post_command.connect(self.post_receiver) diff --git a/django_structlog/middlewares/__init__.py b/django_structlog/middlewares/__init__.py index 55da6f6b39443ef362e256c13e034de9372e251a..d9d9b02e13d8a07b7dd4f23a6514674692e7f6f7 100644 --- a/django_structlog/middlewares/__init__.py +++ b/django_structlog/middlewares/__init__.py @@ -1 +1,5 @@ from .request import RequestMiddleware # noqa F401 + +__all__ = [ + "RequestMiddleware", +] diff --git a/django_structlog/middlewares/request.py b/django_structlog/middlewares/request.py index f17a7be17f8a907339b68c66b266aa9931cb137b..e0ac48d140d4e28683da9c6e475e6777c6330652 100644 --- a/django_structlog/middlewares/request.py +++ b/django_structlog/middlewares/request.py @@ -1,27 +1,59 @@ import asyncio import logging +import sys import uuid +from typing import ( + TYPE_CHECKING, + Any, + AsyncGenerator, + AsyncIterator, + Awaitable, + Callable, + Generator, + Iterator, + Type, + Union, + cast, +) import structlog -from asgiref.sync import iscoroutinefunction, markcoroutinefunction +from asgiref import sync from django.core.exceptions import PermissionDenied +from django.core.signals import got_request_exception from django.http import Http404, StreamingHttpResponse -from asgiref import sync from .. import signals from ..app_settings import app_settings +if sys.version_info >= (3, 12, 0): + from inspect import ( # type: ignore[attr-defined] + iscoroutinefunction, + markcoroutinefunction, + ) +else: + from asgiref.sync import ( # type: ignore[no-redef] + iscoroutinefunction, + markcoroutinefunction, + ) + +if TYPE_CHECKING: # pragma: no cover + from types import TracebackType + + from django.http import HttpRequest, HttpResponse + logger = structlog.getLogger(__name__) -def get_request_header(request, header_key, meta_key): +def get_request_header(request: "HttpRequest", header_key: str, meta_key: str) -> Any: if hasattr(request, "headers"): return request.headers.get(header_key) return request.META.get(meta_key) -def sync_streaming_content_wrapper(streaming_content, context): +def sync_streaming_content_wrapper( + streaming_content: Iterator[bytes], context: Any +) -> Generator[bytes, None, None]: with structlog.contextvars.bound_contextvars(**context): logger.info("streaming_started") try: @@ -29,11 +61,14 @@ def sync_streaming_content_wrapper(streaming_content, context): yield chunk except Exception: logger.exception("streaming_failed") + raise else: logger.info("streaming_finished") -async def async_streaming_content_wrapper(streaming_content, context): +async def async_streaming_content_wrapper( + streaming_content: AsyncIterator[bytes], context: Any +) -> AsyncGenerator[bytes, Any]: with structlog.contextvars.bound_contextvars(**context): logger.info("streaming_started") try: @@ -44,6 +79,7 @@ async def async_streaming_content_wrapper(streaming_content, context): raise except Exception: logger.exception("streaming_failed") + raise else: logger.info("streaming_finished") @@ -61,30 +97,38 @@ class RequestMiddleware: sync_capable = True async_capable = True - def __init__(self, get_response): + def __init__( + self, + get_response: Callable[ + ["HttpRequest"], Union["HttpResponse", Awaitable["HttpResponse"]] + ], + ) -> None: self.get_response = get_response if iscoroutinefunction(self.get_response): markcoroutinefunction(self) + got_request_exception.connect(self.process_got_request_exception) - def __call__(self, request): + def __call__( + self, request: "HttpRequest" + ) -> Union["HttpResponse", Awaitable["HttpResponse"]]: if iscoroutinefunction(self): - return self.__acall__(request) + return cast(RequestMiddleware, self).__acall__(request) self.prepare(request) - response = self.get_response(request) + response = cast("HttpResponse", self.get_response(request)) self.handle_response(request, response) return response - async def __acall__(self, request): + async def __acall__(self, request: "HttpRequest") -> "HttpResponse": await sync.sync_to_async(self.prepare)(request) try: - response = await self.get_response(request) + response = await cast(Awaitable["HttpResponse"], self.get_response(request)) except asyncio.CancelledError: logger.warning("request_cancelled") raise await sync.sync_to_async(self.handle_response)(request, response) return response - def handle_response(self, request, response): + def handle_response(self, request: "HttpRequest", response: "HttpResponse") -> None: if not hasattr(request, "_raised_exception"): self.bind_user_id(request) context = structlog.contextvars.get_merged_contextvars(logger) @@ -113,15 +157,13 @@ class RequestMiddleware: ) if isinstance(response, StreamingHttpResponse): streaming_content = response.streaming_content - try: - iter(streaming_content) - except TypeError: + if response.is_async: response.streaming_content = async_streaming_content_wrapper( - streaming_content, context + cast(AsyncIterator[bytes], streaming_content), context ) else: response.streaming_content = sync_streaming_content_wrapper( - streaming_content, context + cast(Iterator[bytes], streaming_content), context ) else: @@ -136,8 +178,8 @@ class RequestMiddleware: ) structlog.contextvars.clear_contextvars() - def prepare(self, request): - from ipware import get_client_ip + def prepare(self, request: "HttpRequest") -> None: + from ipware import get_client_ip # type: ignore[import-untyped] request_id = get_request_header( request, "x-request-id", "HTTP_X_REQUEST_ID" @@ -161,11 +203,11 @@ class RequestMiddleware: logger.info("request_started", **log_kwargs) @staticmethod - def format_request(request): + def format_request(request: "HttpRequest") -> str: return f"{request.method} {request.get_full_path()}" @staticmethod - def bind_user_id(request): + def bind_user_id(request: "HttpRequest") -> None: user_id_field = app_settings.USER_ID_FIELD if hasattr(request, "user") and request.user is not None and user_id_field: user_id = None @@ -175,7 +217,17 @@ class RequestMiddleware: user_id = str(user_id) structlog.contextvars.bind_contextvars(user_id=user_id) - def process_exception(self, request, exception): + def process_got_request_exception( + self, sender: Type[Any], request: "HttpRequest", **kwargs: Any + ) -> None: + if not hasattr(request, "_raised_exception"): + ex = cast( + tuple[Type[Exception], Exception, "TracebackType"], + sys.exc_info(), + ) + self._process_exception(request, ex[1]) + + def _process_exception(self, request: "HttpRequest", exception: Exception) -> None: if isinstance(exception, (Http404, PermissionDenied)): # We don't log an exception here, and we don't set that we handled # an error as we want the standard `request_finished` log message diff --git a/django_structlog/py.typed b/django_structlog/py.typed new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/django_structlog/signals.py b/django_structlog/signals.py index 2a08e14366266329ac985e53cc20666929f5ac85..9893f4401a4db0e4b4abd9531a1af742435baff5 100644 --- a/django_structlog/signals.py +++ b/django_structlog/signals.py @@ -1,6 +1,5 @@ import django.dispatch - bind_extra_request_metadata = django.dispatch.Signal() """ Signal to add extra ``structlog`` bindings from ``django``'s request. diff --git a/django_structlog_demo_project/home/api_views.py b/django_structlog_demo_project/home/api_views.py index 857bc74e97f5d960a5231d68b392fdb2832c63e7..4720899464ab98336f820a5be4ed1b7f6ae2e345 100644 --- a/django_structlog_demo_project/home/api_views.py +++ b/django_structlog_demo_project/home/api_views.py @@ -1,6 +1,6 @@ +import structlog from rest_framework.decorators import api_view from rest_framework.response import Response -import structlog logger = structlog.get_logger(__name__) diff --git a/django_structlog_demo_project/home/ninja_views.py b/django_structlog_demo_project/home/ninja_views.py index 31c19a5731e16ef545273a81b536ed9b65dea257..e37dfcd52dd3329dba0cd1efbb04120b73612929 100644 --- a/django_structlog_demo_project/home/ninja_views.py +++ b/django_structlog_demo_project/home/ninja_views.py @@ -14,7 +14,7 @@ class OptionalSessionAuth(SessionAuth): return request.user -@router.get("/ninja", url_name="add", auth=OptionalSessionAuth()) +@router.get("/ninja", url_name="ninja", auth=OptionalSessionAuth()) def ninja(request): logger.info("This is a ninja structured log") return {"result": "ok"} diff --git a/django_structlog_demo_project/home/views.py b/django_structlog_demo_project/home/views.py index 5ec4d0ffd6fadc14ada5d888ce063ae7bd3e2bd9..4dc746a7a543f0346913de316f4a0493575c06f5 100644 --- a/django_structlog_demo_project/home/views.py +++ b/django_structlog_demo_project/home/views.py @@ -4,11 +4,12 @@ import time import structlog from django.http import HttpResponse, StreamingHttpResponse + from django_structlog_demo_project.taskapp.celery import ( - successful_task, failing_task, nesting_task, rejected_task, + successful_task, ) logger = structlog.get_logger(__name__) @@ -44,9 +45,7 @@ def revoke_task(request): def enqueue_unknown_task(request): - from django_structlog_demo_project.taskapp.celery import ( - unknown_task, - ) + from django_structlog_demo_project.taskapp.celery import unknown_task logger.info("Enqueuing unknown task") unknown_task.delay() diff --git a/django_structlog_demo_project/taskapp/celery.py b/django_structlog_demo_project/taskapp/celery.py index b1d964e00817250b837f9731d77ed89439926590..b946fb8fcbf37815bf1478f4397360c39b7164ba 100644 --- a/django_structlog_demo_project/taskapp/celery.py +++ b/django_structlog_demo_project/taskapp/celery.py @@ -4,7 +4,7 @@ import os import structlog from celery import Celery, shared_task, signals -from django.apps import apps, AppConfig +from django.apps import AppConfig, apps from django.conf import settings from django_structlog.celery.steps import DjangoStructLogInitStep diff --git a/django_structlog_demo_project/users/forms.py b/django_structlog_demo_project/users/forms.py index 94a3b450ec583b5796399e24474b09685f2a2e25..cf4b25a56ab4ee6cf08469072c118202306f9486 100644 --- a/django_structlog_demo_project/users/forms.py +++ b/django_structlog_demo_project/users/forms.py @@ -1,4 +1,4 @@ -from django.contrib.auth import get_user_model, forms +from django.contrib.auth import forms, get_user_model from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ diff --git a/django_structlog_demo_project/users/migrations/0001_initial.py b/django_structlog_demo_project/users/migrations/0001_initial.py index 8176245b4466ea2066fb2266f115b5049a8d131f..4d9976d64d8783a03cd4de62d2be85610a2067e1 100644 --- a/django_structlog_demo_project/users/migrations/0001_initial.py +++ b/django_structlog_demo_project/users/migrations/0001_initial.py @@ -1,7 +1,7 @@ import django.contrib.auth.models import django.contrib.auth.validators -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/django_structlog_demo_project/users/tests/test_adapters.py b/django_structlog_demo_project/users/tests/test_adapters.py index 3f611b55b655ce700da113a5d9ba7ca5c902ef8d..44c04db0b6a05ce4e735e3e2ad4db14389d391c2 100644 --- a/django_structlog_demo_project/users/tests/test_adapters.py +++ b/django_structlog_demo_project/users/tests/test_adapters.py @@ -1,8 +1,6 @@ import pytest -from django_structlog_demo_project.users.adapters import ( - AccountAdapter, -) +from django_structlog_demo_project.users.adapters import AccountAdapter pytestmark = pytest.mark.django_db diff --git a/django_structlog_demo_project/users/tests/test_urls.py b/django_structlog_demo_project/users/tests/test_urls.py index 2191d12d50eda68b186ba9bfe3c33485ea2b6734..17bb2a407542302c256d7df7828dd2e3e6f36714 100644 --- a/django_structlog_demo_project/users/tests/test_urls.py +++ b/django_structlog_demo_project/users/tests/test_urls.py @@ -1,6 +1,6 @@ import pytest from django.conf import settings -from django.urls import reverse, resolve +from django.urls import resolve, reverse pytestmark = pytest.mark.django_db diff --git a/django_structlog_demo_project/users/urls.py b/django_structlog_demo_project/users/urls.py index ce5351379f1c71313c7c55a1cdb2a6819ae78b6b..3b2c4ac6d1f7700cb51c1b8d0cf008ca282c680e 100644 --- a/django_structlog_demo_project/users/urls.py +++ b/django_structlog_demo_project/users/urls.py @@ -1,10 +1,10 @@ from django.urls import re_path from django_structlog_demo_project.users.views import ( + user_detail_view, user_list_view, user_redirect_view, user_update_view, - user_detail_view, ) app_name = "users" diff --git a/docker-compose.docs.yml b/docker-compose.docs.yml index e50313566acbf3331ca3c77e096468443e4ec09d..dbb67375f53e62fa6b7ecf34a790f497b47c7b66 100644 --- a/docker-compose.docs.yml +++ b/docker-compose.docs.yml @@ -4,7 +4,7 @@ services: context: . dockerfile: ./compose/local/docs/Dockerfile args: - PYTHON_VERSION: 3.12 + PYTHON_VERSION: 3.13 image: django_structlog_demo_project_docs volumes: - .:/app:cached @@ -12,7 +12,7 @@ services: environment: - SPHINX_COMMAND=html ports: - - "5000:5000" + - "8080:8080" docs-test: image: django_structlog_demo_project_docs volumes: diff --git a/docker-compose.yml b/docker-compose.yml index 6931e5cf7ef468f75441e8998d3ac4fe156f359a..c6a90b00545be500d71b410f78e33977e9c60ab6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: context: . dockerfile: ./compose/local/django/Dockerfile args: - PYTHON_VERSION: 3.12 + PYTHON_VERSION: 3.13 image: django_structlog_demo_project_local_django depends_on: - postgres diff --git a/docs/changelog.rst b/docs/changelog.rst index e1b9012bf85d6afef61928cd96104cec375fde9d..88035702eca9b9b19506312ebbf42c100479c2d3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,11 +1,38 @@ Change Log ========== +9.0.1 (January 29, 2024) +------------------------ + +*Fixes:* + - Fix exceptions not being propagated when using streaming response. See `#747 <https://github.com/jrobichaud/django-structlog/pull/747>`_. Special thanks to `@liambuchanan <https://github.com/liambuchanan>`_. + + +9.0.0 (November 26, 2024) +------------------------- + +See: :ref:`upgrade_9.0` + +*New:* + - Add type definitions for the project. See `#697 <https://github.com/jrobichaud/django-structlog/pull/697>`_ and `#696 <https://github.com/jrobichaud/django-structlog/issues/696>`_. Special thanks to `@j00bar <https://github.com/j00bar>`_ and `@MaxDude132 <https://github.com/MaxDude132>`_ for the review. + +*Changes:* + - ``RequestMiddleware`` now relies on django signal `got_request_exception <https://docs.djangoproject.com/en/dev/ref/signals/#got-request-exception>`_ instead of Middleware `process_exception <https://docs.djangoproject.com/en/dev/topics/http/middleware/#process-exception>`_ method. See `#705 <https://github.com/jrobichaud/django-structlog/pull/705>`_, `#658 <https://github.com/jrobichaud/django-structlog/issues/658>`_ and :ref:`upgrade_9.0`. Special thanks to `@sshishov <https://github.com/sshishov>`_. + - Add python 3.13 support. See `#674 <https://github.com/jrobichaud/django-structlog/pull/674>`_. + - Drop python 3.8 support. See `#674 <https://github.com/jrobichaud/django-structlog/pull/674>`_. + - Django 5.1 and celery 5.4 support. See `#617 <https://github.com/jrobichaud/django-structlog/pull/617>`_. + +*Other:* + - Migrated project to use python 3.13 along with readthedocs generation. + - now use `isort <https://pycqa.github.io/isort/>`_ + - fixed ``codecov`` github action that was not properly configured and therefore not properly reporting coverage. + + 8.1.0 (May 24, 2024) -------------------- *New:* - - Add a :ref:`setting <settings>` ``DJANGO_STRUCTLOG_USER_ID_FIELD = 'pk'`` to customize what user field to use as ``user_id`` in the logs. `#546 <https://github.com/jrobichaud/django-structlog/pull/546>`_ and `#545 <https://github.com/jrobichaud/django-structlog/issues/545>`_. Special thanks to `@sshishov <https://github.com/ sshishov>`_. + - Add a :ref:`setting <settings>` ``DJANGO_STRUCTLOG_USER_ID_FIELD = 'pk'`` to customize what user field to use as ``user_id`` in the logs. See `#546 <https://github.com/jrobichaud/django-structlog/pull/546>`_ and `#545 <https://github.com/jrobichaud/django-structlog/issues/545>`_. Special thanks to `@sshishov <https://github.com/sshishov>`_. *Changes:* - Drop support of python 3.7 diff --git a/docs/conf.py b/docs/conf.py index cda928847d05ef1806ac522b2e66c5fed2030c18..d024c18e10eabd4aaa92a63c78623db7d49ccf5e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,5 @@ -import sys import os +import sys sys.path.append(os.path.join(os.path.dirname(__file__), "..")) diff --git a/docs/development.rst b/docs/development.rst index 11bc9e731b6c91cff12846e7d74fedafefc65b65..5874137352a27929d4ba416eec4ff64374e8b0e0 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -45,4 +45,4 @@ Building, Serving and Testing the Documentation Locally .. code-block:: bash $ docker compose -p django-structlog-docs -f docker-compose.docs.yml up --build - Serving on http://127.0.0.1:5000 + Serving on http://127.0.0.1:8080 diff --git a/docs/how_tos.rst b/docs/how_tos.rst index ab1652f2d08c9774049b7bbcd6cab9a8de58cdcb..c581ba3ffbbeb7e3753598638813621eee2480ca 100644 --- a/docs/how_tos.rst +++ b/docs/how_tos.rst @@ -100,7 +100,7 @@ Origin: `#412 <https://github.com/jrobichaud/django-structlog/issues/412>`_ self.excluded_event_type = excluded_event_type def filter(self, record): - if not isinstance(msg, dict) or self.excluded_event_type is None: + if not isinstance(record.msg, dict) or self.excluded_event_type is None: return True # Include the log message if msg is not a dictionary or excluded_event_type is not provided if record.msg.get('event') in self.excluded_event_type: diff --git a/docs/requirements.txt b/docs/requirements.txt index 1764f04f4e649017828eb6fb2dad269122802ce5..b97bbcadea06e91efd2fdb78aec3064b38e7ef31 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,8 @@ -sphinx==7.3.7 -sphinx_rtd_theme==2.0.0 +sphinx==8.1.3 +sphinx_rtd_theme==3.0.2 celery==5.4.0 django>=4.2,<6 structlog -sphinx-autobuild==2024.4.16 -Jinja2==3.1.4 -importlib-metadata>=1,<8 +sphinx-autobuild==2024.10.3 +Jinja2==3.1.5 +importlib-metadata>=8.0.0,<9 diff --git a/pyproject.toml b/pyproject.toml index e0d8cc60ae1bf7c6516a6bf1670cfa0aeb42a449..d97440c780b032c2b4034a9d2d3b220dd5a74f04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" ] readme = "README.rst" dynamic = ["version"] - requires-python = ">=3.8" + requires-python = ">=3.9" license = { text = "MIT" } dependencies = [ "django>=4.2", @@ -23,16 +23,18 @@ build-backend = "setuptools.build_meta" "Framework :: Django", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: System :: Logging", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", + "Typing :: Typed", ] [project.urls] @@ -62,11 +64,11 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 88 target-version = [ - 'py38', 'py39', 'py310', 'py311', 'py312', + 'py313', ] include = '\.pyi?$' exclude = ''' @@ -84,7 +86,7 @@ build-backend = "setuptools.build_meta" [tool.ruff] line-length = 88 - target-version = "py312" + target-version = "py313" lint.ignore = [ 'E501', ] @@ -99,17 +101,18 @@ build-backend = "setuptools.build_meta" # # Also, make sure that all python versions used here are included in ./github/worksflows/main.yml envlist = - py{38,39,310,311}-django42-celery5{2,3}-redis{3,4}-kombu5, - py31{0,1}-django50-celery53-redis4-kombu5, - py312-django{42,50}-celery53-redis4-kombu5, + py{39,310,311}-django42-celery5{2,3}-redis{3,4}-kombu5, + py31{0,1}-django5{0,1}-celery5{3,4}-redis4-kombu5, + py312-django{42,50,51}-celery5{3,4}-redis4-kombu5, + py313-django{51}-celery5{3,4}-redis4-kombu5, [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 [testenv] setenv = @@ -125,8 +128,10 @@ build-backend = "setuptools.build_meta" celery51: Celery >=5.1, <5.2 celery52: Celery >=5.2, <5.3 celery53: Celery >=5.3, <5.4 + celery54: Celery >=5.4, <5.5 django42: Django >=4.2, <5.0 django50: Django >=5.0, <5.1 + django51: Django >=5.1, <5.2 -r{toxinidir}/requirements/ci.txt commands = pytest --cov=./test_app --cov=./django_structlog --cov-append test_app @@ -148,3 +153,15 @@ include = [ "./django_structlog_demo_project/*", "./test_app/*", ] + +[tool.mypy] +python_version=3.9 +strict=true +packages=[ + "django_structlog", + "test_app", +] + +[tool.isort] +profile = "black" +filter_files = true diff --git a/requirements/black.txt b/requirements/black.txt index 033748b7e14523e59bd447c98a6e0547bd0fe6e6..dfa81c4901875f872810da7062211c0cea0953e4 100644 --- a/requirements/black.txt +++ b/requirements/black.txt @@ -1 +1 @@ -black==24.4.2 # https://github.com/ambv/black +black==24.10.0 # https://github.com/ambv/black diff --git a/requirements/ci.txt b/requirements/ci.txt index 4abc02d4f3eeb5b015ad0cfb76b2a515f2c5b4cc..58cc38a38d9ade43eb62ad83940edc620b9b742f 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -1,32 +1,32 @@ # Django # ------------------------------------------------------------------------------ -django-environ==0.11.2 # https://github.com/joke2k/django-environ +django-environ==0.12.0 # https://github.com/joke2k/django-environ django-redis==5.4.0 # https://github.com/niwinz/django-redis django-extensions==3.2.3 structlog>=21.4.0 colorama>=0.4.3 -psycopg2-binary==2.9.9 # https://github.com/psycopg/psycopg2 +psycopg[binary]==3.2.4 # https://github.com/psycopg/psycopg # Testing # ------------------------------------------------------------------------------ -pytest==8.2.1 # https://github.com/pytest-dev/pytest +pytest==8.3.4 # https://github.com/pytest-dev/pytest pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar -pytest-cov==5.0.0 +pytest-cov==6.0.0 # Code quality # ------------------------------------------------------------------------------ -coverage==7.5.1 # https://github.com/nedbat/coveragepy -pylint-django==2.5.5 # https://github.com/PyCQA/pylint-django +-r coverage.txt +pylint-django==2.6.1 # https://github.com/PyCQA/pylint-django pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery # Django # ------------------------------------------------------------------------------ -factory-boy==3.3.0 # https://github.com/FactoryBoy/factory_boy +factory-boy==3.3.1 # https://github.com/FactoryBoy/factory_boy django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin -pytest-django==4.8.0 # https://github.com/pytest-dev/pytest-django +pytest-django==4.9.0 # https://github.com/pytest-dev/pytest-django # Setup tools # ------------------------------------------------------------------------------ diff --git a/requirements/coverage.txt b/requirements/coverage.txt new file mode 100644 index 0000000000000000000000000000000000000000..396b98ac97df905ba61e42acbb44ce3eafd07766 --- /dev/null +++ b/requirements/coverage.txt @@ -0,0 +1 @@ +coverage==7.6.10 # https://github.com/nedbat/coveragepy diff --git a/requirements/deployment.txt b/requirements/deployment.txt index 7872cbbd8b949c8ac70bfe5574b9a7e87c587b6a..a358e18d996f7524c728088684a499a0d410b6c1 100644 --- a/requirements/deployment.txt +++ b/requirements/deployment.txt @@ -1 +1 @@ -importlib-metadata>=1,<8 +importlib-metadata>=8.0.0,<9 diff --git a/requirements/isort.txt b/requirements/isort.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/requirements/local-base.txt b/requirements/local-base.txt index 483a99dc9f40ac795dbc988709c48aea56afc593..126adecdfccfc446128e2434c6001f2ee2c2dab4 100644 --- a/requirements/local-base.txt +++ b/requirements/local-base.txt @@ -1,56 +1,56 @@ -pytz==2024.1 # https://github.com/stub42/pytz +pytz==2024.2 # https://github.com/stub42/pytz python-slugify==8.0.4 # https://github.com/un33k/python-slugify # Django # ------------------------------------------------------------------------------ -django==5.0.6 # https://www.djangoproject.com/ -django-environ==0.11.2 # https://github.com/joke2k/django-environ -django-model-utils==4.5.1 # https://github.com/jazzband/django-model-utils -django-allauth==0.63.1 # https://github.com/pennersr/django-allauth -django-crispy-forms==2.1 # https://github.com/django-crispy-forms/django-crispy-forms -crispy-bootstrap5==2024.2 # https://github.com/django-crispy-forms/crispy-bootstrap5 +django==5.1.5 # https://www.djangoproject.com/ +django-environ==0.12.0 # https://github.com/joke2k/django-environ +django-model-utils==5.0.0 # https://github.com/jazzband/django-model-utils +django-allauth==65.3.1 # https://github.com/pennersr/django-allauth +django-crispy-forms==2.3 # https://github.com/django-crispy-forms/django-crispy-forms +crispy-bootstrap5==2024.10 # https://github.com/django-crispy-forms/crispy-bootstrap5 django-redis==5.4.0 # https://github.com/niwinz/django-redis asgiref==3.8.1 # https://github.com/django/asgiref # Django REST Framework -djangorestframework==3.15.1 # https://github.com/encode/django-rest-framework +djangorestframework==3.15.2 # https://github.com/encode/django-rest-framework coreapi==2.3.3 # https://github.com/core-api/python-client # django-ninja -django-ninja==1.1.0 # https://github.com/vitalik/django-ninja +django-ninja==1.3.0 # https://github.com/vitalik/django-ninja -structlog==24.1.0 +structlog==25.1.0 colorama==0.4.6 django-ipware==7.0.1 -Werkzeug==3.0.3 # https://github.com/pallets/werkzeug +Werkzeug==3.1.3 # https://github.com/pallets/werkzeug ipdb==0.13.13 # https://github.com/gotcha/ipdb -psycopg2-binary==2.9.9 # https://github.com/psycopg/psycopg2 +psycopg[binary]==3.2.4 # https://github.com/psycopg/psycopg # Testing # ------------------------------------------------------------------------------ -pytest==8.2.1 # https://github.com/pytest-dev/pytest +pytest==8.3.4 # https://github.com/pytest-dev/pytest pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar -pytest-cov==5.0.0 -pytest-asyncio==0.23.7 # https://github.com/pytest-dev/pytest-asyncio +pytest-cov==6.0.0 +pytest-asyncio==0.25.2 # https://github.com/pytest-dev/pytest-asyncio pytest-mock==3.14.0 # https://github.com/pytest-dev/pytest-mock # Code quality # ------------------------------------------------------------------------------ -r ruff.txt -coverage==7.5.1 # https://github.com/nedbat/coveragepy +-r coverage.txt -r black.txt -pylint-django==2.5.5 # https://github.com/PyCQA/pylint-django +pylint-django==2.6.1 # https://github.com/PyCQA/pylint-django pylint-celery==0.3 # https://github.com/PyCQA/pylint-celery # Django # ------------------------------------------------------------------------------ -factory-boy==3.3.0 # https://github.com/FactoryBoy/factory_boy +factory-boy==3.3.1 # https://github.com/FactoryBoy/factory_boy django-extensions==3.2.3 # https://github.com/django-extensions/django-extensions django-coverage-plugin==3.1.0 # https://github.com/nedbat/django_coverage_plugin -pytest-django==4.8.0 # https://github.com/pytest-dev/pytest-django +pytest-django==4.9.0 # https://github.com/pytest-dev/pytest-django # pre-commit # ------------------------------------------------------------------------------ -pre-commit==3.7.1 # https://github.com/pre-commit/pre-commit +pre-commit==4.1.0 # https://github.com/pre-commit/pre-commit diff --git a/requirements/local.txt b/requirements/local.txt index cc81fb0b545af28a4d2fb9fa429a379da5edc47f..69e860d803b85bd0437333f518064f5a59420c6f 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -1,9 +1,9 @@ -r local-base.txt -redis==5.0.4 # https://github.com/antirez/redis +redis==5.2.1 # https://github.com/antirez/redis celery==5.4.0 # pyup: < 5.0 # https://github.com/celery/celery -kombu==5.3.7 +kombu==5.4.2 flower==2.0.1 # https://github.com/mher/flower -uvicorn==0.29.0 # https://github.com/encode/uvicorn -gunicorn==22.0.0 # https://github.com/benoitc/gunicorn -amqp==5.2.0 # https://github.com/celery/py-amqp +uvicorn==0.34.0 # https://github.com/encode/uvicorn +gunicorn==23.0.0 # https://github.com/benoitc/gunicorn +amqp==5.3.1 # https://github.com/celery/py-amqp diff --git a/requirements/mypy.txt b/requirements/mypy.txt new file mode 100644 index 0000000000000000000000000000000000000000..30ec0b6bdb860894261e1664320eff540a3ce81b --- /dev/null +++ b/requirements/mypy.txt @@ -0,0 +1,3 @@ +mypy==1.14.1 +celery-types==0.22.0 +django-stubs[compatible-mypy]==5.1.2 diff --git a/requirements/ruff.txt b/requirements/ruff.txt index 8c0d9cd9aca9a6bfc636acee371cbdb0bda4434a..c669d013c556aa38a63076e8bf15d06e9843a3b9 100644 --- a/requirements/ruff.txt +++ b/requirements/ruff.txt @@ -1 +1 @@ -ruff==0.4.4 # https://github.com/astral-sh/ruff +ruff==0.9.3 # https://github.com/astral-sh/ruff diff --git a/test_app/tests/celery/test_receivers.py b/test_app/tests/celery/test_receivers.py index 4ed0228457fd19879c0c920012f765a06d1dd66f..16d97392ee1d57182d7d6d09633a771aa14a0d2d 100644 --- a/test_app/tests/celery/test_receivers.py +++ b/test_app/tests/celery/test_receivers.py @@ -1,32 +1,33 @@ import logging from signal import SIGTERM -from unittest.mock import Mock, patch, call, MagicMock +from typing import Any, Optional, Type +from unittest.mock import MagicMock, Mock, call, patch import structlog from celery import shared_task from django.contrib.auth.models import AnonymousUser from django.dispatch import receiver as django_receiver -from django.test import TestCase, RequestFactory +from django.test import RequestFactory, TestCase from django_structlog.celery import receivers, signals class TestReceivers(TestCase): - def setUp(self): + def setUp(self) -> None: self.factory = RequestFactory() self.logger = structlog.getLogger(__name__) - def tearDown(self): + def tearDown(self) -> None: structlog.contextvars.clear_contextvars() - def test_defer_task(self): + def test_defer_task(self) -> None: expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() @shared_task - def test_task(value): # pragma: no cover + def test_task(value: Any) -> None: # pragma: no cover pass receiver = receivers.CeleryReceiver() @@ -37,18 +38,18 @@ class TestReceivers(TestCase): ) as log_results: test_task.delay("foo") self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_enqueued", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("child_task_id", record.msg) self.assertEqual(expected_uuid, record.msg["request_id"]) - def test_receiver_before_task_publish_celery_protocol_v2(self): + def test_receiver_before_task_publish_celery_protocol_v2(self) -> None: expected_uuid = "00000000-0000-0000-0000-000000000000" expected_user_id = "1234" expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111" - headers = {} + headers: dict[str, Any] = {} structlog.contextvars.bind_contextvars( request_id=expected_uuid, user_id=expected_user_id, @@ -68,13 +69,13 @@ class TestReceivers(TestCase): headers, ) - def test_receiver_before_task_publish_celery_protocol_v1(self): + def test_receiver_before_task_publish_celery_protocol_v1(self) -> None: """Protocol v1 does not allow to store metadata""" expected_uuid = "00000000-0000-0000-0000-000000000000" expected_user_id = "1234" expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111" - headers = {} + headers: dict[str, Any] = {} structlog.contextvars.bind_contextvars( request_id=expected_uuid, user_id=expected_user_id, @@ -93,20 +94,27 @@ class TestReceivers(TestCase): headers, ) - def test_signal_modify_context_before_task_publish_celery_protocol_v2(self): + def test_signal_modify_context_before_task_publish_celery_protocol_v2(self) -> None: expected_uuid = "00000000-0000-0000-0000-000000000000" user_id = "1234" expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111" routing_key = "foo" - properties = {"correlation_id": "22222222-2222-2222-2222-222222222222"} + properties: dict[str, Optional[str]] = { + "correlation_id": "22222222-2222-2222-2222-222222222222" + } - received_properties = None - received_routing_key = None + received_properties: Any = None + received_routing_key: Any = None @django_receiver(signals.modify_context_before_task_publish) def receiver_modify_context_before_task_publish( - sender, signal, context, task_properties, task_routing_key, **kwargs - ): + sender: Type[Any], + signal: Any, + context: Any, + task_properties: Any, + task_routing_key: str, + **kwargs: Any, + ) -> None: keys_to_keep = {"request_id", "parent_task_id"} new_dict = { key_to_keep: context[key_to_keep] @@ -120,7 +128,7 @@ class TestReceivers(TestCase): nonlocal received_routing_key received_routing_key = task_routing_key - headers = {} + headers: dict[str, Any] = {} structlog.contextvars.bind_contextvars( request_id=expected_uuid, user_id=user_id, @@ -149,10 +157,10 @@ class TestReceivers(TestCase): ) self.assertEqual("foo", received_routing_key) - def test_receiver_after_task_publish(self): + def test_receiver_after_task_publish(self) -> None: expected_task_id = "00000000-0000-0000-0000-000000000000" expected_task_name = "Foo" - headers = {"id": expected_task_id, "task": expected_task_name} + headers: dict[str, Any] = {"id": expected_task_id, "task": expected_task_name} receiver = receivers.CeleryReceiver() with self.assertLogs( logging.getLogger("django_structlog.celery.receivers"), logging.INFO @@ -160,7 +168,7 @@ class TestReceivers(TestCase): receiver.receiver_after_task_publish(headers=headers) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_enqueued", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("child_task_id", record.msg) @@ -168,10 +176,10 @@ class TestReceivers(TestCase): self.assertIn("child_task_name", record.msg) self.assertEqual(expected_task_name, record.msg["child_task_name"]) - def test_receiver_after_task_publish_protocol_v1(self): + def test_receiver_after_task_publish_protocol_v1(self) -> None: expected_task_id = "00000000-0000-0000-0000-000000000000" expected_task_name = "Foo" - body = {"id": expected_task_id, "task": expected_task_name} + body: dict[str, Any] = {"id": expected_task_id, "task": expected_task_name} receiver = receivers.CeleryReceiver() with self.assertLogs( @@ -180,7 +188,7 @@ class TestReceivers(TestCase): receiver.receiver_after_task_publish(body=body) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_enqueued", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("child_task_id", record.msg) @@ -188,7 +196,7 @@ class TestReceivers(TestCase): self.assertIn("child_task_name", record.msg) self.assertEqual(expected_task_name, record.msg["child_task_name"]) - def test_receiver_task_pre_run(self): + def test_receiver_task_pre_run(self) -> None: expected_request_uuid = "00000000-0000-0000-0000-000000000000" task_id = "11111111-1111-1111-1111-111111111111" expected_user_id = "1234" @@ -220,17 +228,17 @@ class TestReceivers(TestCase): ) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_started", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("task", record.msg) self.assertEqual("task_name", record.msg["task"]) - def test_signal_bind_extra_task_metadata(self): + def test_signal_bind_extra_task_metadata(self) -> None: @django_receiver(signals.bind_extra_task_metadata) def receiver_bind_extra_request_metadata( - sender, signal, task=None, logger=None - ): + sender: Type[Any], signal: Any, task: Any = None, logger: Any = None + ) -> None: structlog.contextvars.bind_contextvars( correlation_id=task.request.correlation_id ) @@ -251,7 +259,7 @@ class TestReceivers(TestCase): self.assertEqual(context["correlation_id"], expected_correlation_uuid) self.assertEqual(context["task_id"], task_id) - def test_receiver_task_retry(self): + def test_receiver_task_retry(self) -> None: expected_reason = "foo" receiver = receivers.CeleryReceiver() @@ -261,19 +269,23 @@ class TestReceivers(TestCase): receiver.receiver_task_retry(reason=expected_reason) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_retrying", record.msg["event"]) self.assertEqual("WARNING", record.levelname) self.assertIn("reason", record.msg) self.assertEqual(expected_reason, record.msg["reason"]) - def test_receiver_task_success(self): + def test_receiver_task_success(self) -> None: expected_result = "foo" @django_receiver(signals.pre_task_succeeded) def receiver_pre_task_succeeded( - sender, signal, task=None, logger=None, result=None - ): + sender: Type[Any], + signal: Any, + task: Any = None, + logger: Any = None, + result: Any = None, + ) -> None: structlog.contextvars.bind_contextvars(result=result) receiver = receivers.CeleryReceiver() @@ -283,13 +295,13 @@ class TestReceivers(TestCase): receiver.receiver_task_success(result=expected_result) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_succeeded", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("result", record.msg) self.assertEqual(expected_result, record.msg["result"]) - def test_receiver_task_failure(self): + def test_receiver_task_failure(self) -> None: expected_exception = "foo" receiver = receivers.CeleryReceiver() with self.assertLogs( @@ -298,14 +310,14 @@ class TestReceivers(TestCase): receiver.receiver_task_failure(exception=Exception("foo")) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_failed", record.msg["event"]) self.assertEqual("ERROR", record.levelname) self.assertIn("error", record.msg) self.assertIn("exception", record.msg) self.assertEqual(expected_exception, record.msg["error"]) - def test_receiver_task_failure_with_throws(self): + def test_receiver_task_failure_with_throws(self) -> None: expected_exception = "foo" mock_sender = Mock() @@ -319,14 +331,14 @@ class TestReceivers(TestCase): ) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_failed", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("error", record.msg) self.assertNotIn("exception", record.msg) self.assertEqual(expected_exception, record.msg["error"]) - def test_receiver_task_revoked(self): + def test_receiver_task_revoked(self) -> None: expected_request_uuid = "00000000-0000-0000-0000-000000000000" task_id = "11111111-1111-1111-1111-111111111111" expected_user_id = "1234" @@ -348,7 +360,7 @@ class TestReceivers(TestCase): ) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_revoked", record.msg["event"]) self.assertEqual("WARNING", record.levelname) self.assertIn("terminated", record.msg) @@ -368,7 +380,7 @@ class TestReceivers(TestCase): self.assertIn("user_id", record.msg) self.assertEqual(expected_user_id, record.msg["user_id"]) - def test_receiver_task_revoked_terminated(self): + def test_receiver_task_revoked_terminated(self) -> None: expected_request_uuid = "00000000-0000-0000-0000-000000000000" task_id = "11111111-1111-1111-1111-111111111111" expected_user_id = "1234" @@ -390,7 +402,7 @@ class TestReceivers(TestCase): ) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_revoked", record.msg["event"]) self.assertEqual("WARNING", record.levelname) self.assertIn("terminated", record.msg) @@ -410,7 +422,7 @@ class TestReceivers(TestCase): self.assertIn("user_id", record.msg) self.assertEqual(expected_user_id, record.msg["user_id"]) - def test_receiver_task_unknown(self): + def test_receiver_task_unknown(self) -> None: task_id = "11111111-1111-1111-1111-111111111111" expected_task_name = "task_name" @@ -421,7 +433,7 @@ class TestReceivers(TestCase): receiver.receiver_task_unknown(id=task_id, name=expected_task_name) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_not_found", record.msg["event"]) self.assertEqual("ERROR", record.levelname) self.assertIn("task_id", record.msg) @@ -429,7 +441,7 @@ class TestReceivers(TestCase): self.assertIn("task", record.msg) self.assertEqual(expected_task_name, record.msg["task"]) - def test_receiver_task_rejected(self): + def test_receiver_task_rejected(self) -> None: task_id = "11111111-1111-1111-1111-111111111111" message = Mock(name="message") message.properties = dict(correlation_id=task_id) @@ -441,13 +453,13 @@ class TestReceivers(TestCase): receiver.receiver_task_rejected(message=message) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_rejected", record.msg["event"]) self.assertEqual("ERROR", record.levelname) self.assertIn("task_id", record.msg) self.assertEqual(task_id, record.msg["task_id"]) - def test_priority(self): + def test_priority(self) -> None: expected_uuid = "00000000-0000-0000-0000-000000000000" user_id = "1234" expected_parent_task_uuid = "11111111-1111-1111-1111-111111111111" @@ -455,7 +467,7 @@ class TestReceivers(TestCase): expected_priority = 6 properties = {"priority": expected_priority} - headers = {} + headers: dict[str, Any] = {} structlog.contextvars.bind_contextvars( request_id=expected_uuid, user_id=user_id, @@ -492,7 +504,7 @@ class TestReceivers(TestCase): ) self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("task_enqueued", record.msg["event"]) self.assertEqual("INFO", record.levelname) self.assertIn("child_task_id", record.msg) @@ -508,17 +520,17 @@ class TestReceivers(TestCase): class TestConnectCeleryTaskSignals(TestCase): - def test_call(self): + def test_call(self) -> None: from celery.signals import ( - before_task_publish, after_task_publish, + before_task_publish, + task_failure, task_prerun, + task_rejected, task_retry, - task_success, - task_failure, task_revoked, + task_success, task_unknown, - task_rejected, ) from django_structlog.celery.receivers import CeleryReceiver @@ -545,8 +557,9 @@ class TestConnectCeleryTaskSignals(TestCase): class TestConnectCelerySignals(TestCase): - def test_call(self): - from celery.signals import before_task_publish, after_task_publish + def test_call(self) -> None: + from celery.signals import after_task_publish, before_task_publish + from django_structlog.celery.receivers import CeleryReceiver receiver = CeleryReceiver() diff --git a/test_app/tests/celery/test_steps.py b/test_app/tests/celery/test_steps.py index d323987a76dacd030cbf96abda157a21a9ba4aa5..58b687e46183c33c2790d302780eb8f0527514ed 100644 --- a/test_app/tests/celery/test_steps.py +++ b/test_app/tests/celery/test_steps.py @@ -6,7 +6,7 @@ from django_structlog.celery import steps class TestDjangoStructLogInitStep(TestCase): - def test_call(self): + def test_call(self) -> None: with patch( "django_structlog.celery.receivers.CeleryReceiver.connect_worker_signals", autospec=True, diff --git a/test_app/tests/middlewares/test_request.py b/test_app/tests/middlewares/test_request.py index f3255e2a9cba9c020598f2e17a5baf44059c56c8..d8088b6874cd96723c49b5cfc5de4d47653b30a8 100644 --- a/test_app/tests/middlewares/test_request.py +++ b/test_app/tests/middlewares/test_request.py @@ -2,39 +2,45 @@ import asyncio import logging import traceback import uuid +from typing import Any, AsyncGenerator, Awaitable, Generator, Type, cast from unittest import mock -from unittest.mock import patch, Mock, AsyncMock +from unittest.mock import AsyncMock, Mock, patch +import structlog from django.contrib.auth.models import AnonymousUser, User from django.contrib.sites.models import Site from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import PermissionDenied +from django.core.signals import got_request_exception from django.dispatch import receiver from django.http import ( Http404, - HttpResponseNotFound, + HttpRequest, + HttpResponse, HttpResponseForbidden, + HttpResponseNotFound, HttpResponseServerError, StreamingHttpResponse, ) -from django.test import TestCase, RequestFactory, override_settings -import structlog +from django.test import RequestFactory, TestCase, override_settings -from django_structlog import middlewares from django_structlog.middlewares.request import ( + RequestMiddleware, + async_streaming_content_wrapper, get_request_header, sync_streaming_content_wrapper, - async_streaming_content_wrapper, ) from django_structlog.signals import ( - bind_extra_request_metadata, - bind_extra_request_finished_metadata, bind_extra_request_failed_metadata, + bind_extra_request_finished_metadata, + bind_extra_request_metadata, ) class TestRequestMiddleware(TestCase): - def setUp(self): + log_results: Any + + def setUp(self) -> None: self.factory = RequestFactory() self.logger = structlog.getLogger(__name__) self.log_results = None @@ -43,15 +49,15 @@ class TestRequestMiddleware(TestCase): defaults={"domain": "testserver", "name": "django_structlog_demo_project"}, ) - def tearDown(self): + def tearDown(self) -> None: structlog.contextvars.clear_contextvars() - def test_process_request_without_user(self): + def test_process_request_without_user(self) -> None: mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results @@ -59,7 +65,7 @@ class TestRequestMiddleware(TestCase): request = self.factory.get("/foo") - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) with patch("uuid.UUID.__str__", return_value=expected_uuid): middleware(request) @@ -76,21 +82,21 @@ class TestRequestMiddleware(TestCase): record = log_results.records[0] self.assertNotIn("request_id", record.msg) - def test_process_request_with_null_user(self): + def test_process_request_with_null_user(self) -> None: mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results return mock_response request = self.factory.get("/foo") - request.user = None + setattr(request, "user", None) - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) with patch("uuid.UUID.__str__", return_value=expected_uuid): middleware(request) @@ -107,12 +113,12 @@ class TestRequestMiddleware(TestCase): record = log_results.records[0] self.assertNotIn("request_id", record.msg) - def test_process_request_anonymous(self): + def test_process_request_anonymous(self) -> None: mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results @@ -121,7 +127,7 @@ class TestRequestMiddleware(TestCase): request = self.factory.get("/foo") request.user = AnonymousUser() - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) with patch("uuid.UUID.__str__", return_value=expected_uuid): middleware(request) @@ -140,12 +146,12 @@ class TestRequestMiddleware(TestCase): self.assertNotIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) - def test_process_request_user(self): + def test_process_request_user(self) -> None: mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results @@ -153,10 +159,10 @@ class TestRequestMiddleware(TestCase): request = self.factory.get("/foo") - mock_user = User.objects.create() + mock_user: Any = User.objects.create() request.user = mock_user - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) with patch("uuid.UUID.__str__", return_value=expected_uuid): middleware(request) @@ -176,12 +182,12 @@ class TestRequestMiddleware(TestCase): self.assertNotIn("request_id", record.msg) self.assertNotIn("user_id", record.msg) - def test_process_request_user_uuid(self): + def test_process_request_user_uuid(self) -> None: mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results @@ -189,11 +195,11 @@ class TestRequestMiddleware(TestCase): request = self.factory.get("/foo") - mock_user = mock.Mock() + mock_user: Any = mock.Mock() mock_user.pk = uuid.UUID(expected_uuid) request.user = mock_user - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) middleware(request) self.assertEqual(1, len(self.log_results.records)) @@ -204,11 +210,11 @@ class TestRequestMiddleware(TestCase): self.assertIsInstance(record.msg["user_id"], str) self.assertEqual(expected_uuid, record.msg["user_id"]) - def test_process_request_user_without_id(self): + def test_process_request_user_without_id(self) -> None: mock_response = Mock() mock_response.status_code = 200 - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results @@ -219,8 +225,8 @@ class TestRequestMiddleware(TestCase): class SimpleUser: pass - request.user = SimpleUser() - middleware = middlewares.RequestMiddleware(get_response) + request.user = cast(Any, SimpleUser()) + middleware = RequestMiddleware(get_response) middleware(request) self.assertEqual(1, len(self.log_results.records)) @@ -230,26 +236,30 @@ class TestRequestMiddleware(TestCase): self.assertIn("user_id", record.msg) self.assertIsNone(record.msg["user_id"]) - def test_log_user_in_request_finished(self): + def test_log_user_in_request_finished(self) -> None: mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" - mock_user = User.objects.create() + mock_user: Any = User.objects.create() request = self.factory.get("/foo") - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: request.user = mock_user return mock_response - middleware = middlewares.RequestMiddleware(get_response) - with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( - "django_structlog.middlewares.request", logging.INFO - ) as log_results: + middleware = RequestMiddleware(get_response) + with ( + patch("uuid.UUID.__str__", return_value=expected_uuid), + self.assertLogs( + "django_structlog.middlewares.request", logging.INFO + ) as log_results, + ): middleware(request) self.assertEqual(2, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) @@ -267,35 +277,39 @@ class TestRequestMiddleware(TestCase): self.assertIn("user_id", record.msg) self.assertEqual(mock_user.id, record.msg["user_id"]) - def test_log_user_in_request_finished_with_exception(self): + def test_log_user_in_request_finished_with_exception(self) -> None: mock_response = Mock() mock_response.status_code = 200 expected_uuid = "00000000-0000-0000-0000-000000000000" - mock_user = User.objects.create() + mock_user: Any = User.objects.create() request = self.factory.get("/foo") - middleware = middlewares.RequestMiddleware(None) - exception = Exception() - - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: request.user = mock_user try: raise exception - except Exception as e: - middleware.process_exception(request, e) + except Exception: + got_request_exception.send(object, request=request) self.exception_traceback = traceback.format_exc() return mock_response + middleware = RequestMiddleware(get_response) + exception = Exception() + middleware.get_response = get_response - with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( - "django_structlog.middlewares.request", logging.INFO - ) as log_results: + with ( + patch("uuid.UUID.__str__", return_value=expected_uuid), + self.assertLogs( + "django_structlog.middlewares.request", logging.INFO + ) as log_results, + ): middleware(request) self.assertEqual(2, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) @@ -315,16 +329,16 @@ class TestRequestMiddleware(TestCase): self.assertIn("user_id", record.msg) self.assertEqual(mock_user.id, record.msg["user_id"]) - def test_signal_bind_extra_request_metadata(self): + def test_signal_bind_extra_request_metadata(self) -> None: @receiver(bind_extra_request_metadata) def receiver_bind_extra_request_metadata( - sender, - signal, - request=None, - logger=None, - log_kwargs=None, - **kwargs, - ): + sender: Type[Any], + signal: Any, + request: Any = None, + logger: Any = None, + log_kwargs: Any = None, + **kwargs: Any, + ) -> None: current_site = get_current_site(request) log_kwargs["request_started_log"] = "foo" structlog.contextvars.bind_contextvars(domain=current_site.domain) @@ -332,7 +346,7 @@ class TestRequestMiddleware(TestCase): mock_response = Mock() mock_response.status_code = 200 - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results @@ -340,10 +354,10 @@ class TestRequestMiddleware(TestCase): request = self.factory.get("/foo") - mock_user = User.objects.create(email="foo@example.com") + mock_user: Any = User.objects.create(email="foo@example.com") request.user = mock_user - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) with self.assertLogs( "django_structlog.middlewares.request", logging.INFO @@ -368,28 +382,33 @@ class TestRequestMiddleware(TestCase): self.assertEqual("request_finished", record.msg["event"]) self.assertNotIn("request_started_log", record.msg) - def test_signal_bind_extra_request_finished_metadata(self): + def test_signal_bind_extra_request_finished_metadata(self) -> None: mock_response = Mock() mock_response.status_code = 200 @receiver(bind_extra_request_finished_metadata) def receiver_bind_extra_request_finished_metadata( - sender, signal, request=None, logger=None, response=None, log_kwargs=None - ): + sender: Type[Any], + signal: Any, + request: Any = None, + logger: Any = None, + response: Any = None, + log_kwargs: Any = None, + ) -> None: self.assertEqual(response, mock_response) current_site = get_current_site(request) log_kwargs["request_finished_log"] = "foo" structlog.contextvars.bind_contextvars(domain=current_site.domain) - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: return mock_response request = self.factory.get("/foo") - mock_user = User.objects.create(email="foo@example.com") + mock_user: Any = User.objects.create(email="foo@example.com") request.user = mock_user - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) with self.assertLogs( "django_structlog.middlewares.request", logging.INFO @@ -397,6 +416,7 @@ class TestRequestMiddleware(TestCase): middleware(request) self.assertEqual(2, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) @@ -419,19 +439,19 @@ class TestRequestMiddleware(TestCase): self.assertIn("request_finished_log", record.msg) self.assertEqual("foo", record.msg["request_finished_log"]) - def test_signal_bind_extra_request_failed_metadata(self): + def test_signal_bind_extra_request_failed_metadata(self) -> None: expected_exception = Exception() @receiver(bind_extra_request_failed_metadata) def receiver_bind_extra_request_failed_metadata( - sender, - signal, - request=None, - response=None, - logger=None, - exception=None, - log_kwargs=None, - ): + sender: Type[Any], + signal: Any, + request: Any = None, + response: Any = None, + logger: Any = None, + exception: Any = None, + log_kwargs: Any = None, + ) -> None: self.assertEqual(exception, expected_exception) current_site = get_current_site(request) log_kwargs["request_failed_log"] = "foo" @@ -439,18 +459,20 @@ class TestRequestMiddleware(TestCase): request = self.factory.get("/foo") - mock_user = User.objects.create(email="foo@example.com") + mock_user: Any = User.objects.create(email="foo@example.com") request.user = mock_user - middleware = middlewares.RequestMiddleware(None) - mock_response = Mock() - - def get_response(_response): - middleware.process_exception(request, expected_exception) + def get_response(_request: HttpRequest) -> HttpResponse: + try: + raise expected_exception + except Exception: + got_request_exception.send(object, request=request) return mock_response - middleware.get_response = get_response + middleware = RequestMiddleware(get_response) + + mock_response = Mock() with self.assertLogs( "django_structlog.middlewares.request", logging.INFO @@ -458,6 +480,8 @@ class TestRequestMiddleware(TestCase): middleware(request) self.assertEqual(2, len(log_results.records)) + + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) @@ -480,32 +504,34 @@ class TestRequestMiddleware(TestCase): self.assertIn("request_failed_log", record.msg) self.assertEqual("foo", record.msg["request_failed_log"]) - def test_process_request_error(self): + def test_process_request_error(self) -> None: expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() - middleware = middlewares.RequestMiddleware(None) - - exception = Exception("This is an exception") - - def get_response(_response): + def get_response(_request: HttpRequest) -> Any: """Simulate an exception""" try: raise exception - except Exception as e: - middleware.process_exception(request, e) + except Exception: + got_request_exception.send(object, request=request) self.exception_traceback = traceback.format_exc() - middleware.get_response = get_response + middleware = RequestMiddleware(get_response) - with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( - logging.getLogger("django_structlog"), logging.INFO - ) as log_results: + exception = Exception("This is an exception") + + with ( + patch("uuid.UUID.__str__", return_value=expected_uuid), + self.assertLogs( + logging.getLogger("django_structlog"), logging.INFO + ) as log_results, + ): middleware(request) self.assertEqual(2, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) @@ -534,29 +560,33 @@ class TestRequestMiddleware(TestCase): self.assertNotIn("user_id", record.msg) self.assertFalse(hasattr(request, "_raised_exception")) - def test_process_request_403_are_processed_as_regular_requests(self): + def test_process_request_403_are_processed_as_regular_requests(self) -> None: expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() - middleware = middlewares.RequestMiddleware(None) - - exception = PermissionDenied() - - def get_response(_response): - """Simulate an exception""" - middleware.process_exception(request, exception) + def get_response(_request: HttpRequest) -> HttpResponse: + try: + raise exception + except Exception: + got_request_exception.send(object, request=request) return HttpResponseForbidden() - middleware.get_response = get_response + middleware = RequestMiddleware(get_response) - with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( - logging.getLogger("django_structlog"), logging.INFO - ) as log_results: + exception = PermissionDenied() + + with ( + patch("uuid.UUID.__str__", return_value=expected_uuid), + self.assertLogs( + logging.getLogger("django_structlog"), logging.INFO + ) as log_results, + ): middleware(request) self.assertEqual(2, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) @@ -584,29 +614,33 @@ class TestRequestMiddleware(TestCase): self.assertNotIn("user_id", record.msg) self.assertFalse(hasattr(request, "_raised_exception")) - def test_process_request_404_are_processed_as_regular_requests(self): + def test_process_request_404_are_processed_as_regular_requests(self) -> None: expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() - middleware = middlewares.RequestMiddleware(None) - - exception = Http404() - - def get_response(_response): - """Simulate an exception""" - middleware.process_exception(request, exception) + def get_response(_request: HttpRequest) -> HttpResponse: + try: + raise exception + except Exception: + got_request_exception.send(object, request=request) return HttpResponseNotFound() - middleware.get_response = get_response + middleware = RequestMiddleware(get_response) - with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( - logging.getLogger("django_structlog"), logging.INFO - ) as log_results: + exception = Http404() + + with ( + patch("uuid.UUID.__str__", return_value=expected_uuid), + self.assertLogs( + logging.getLogger("django_structlog"), logging.INFO + ) as log_results, + ): middleware(request) self.assertEqual(2, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) @@ -635,29 +669,33 @@ class TestRequestMiddleware(TestCase): self.assertFalse(hasattr(request, "_raised_exception")) @override_settings(DJANGO_STRUCTLOG_STATUS_4XX_LOG_LEVEL=logging.INFO) - def test_process_request_4XX_can_be_personalized(self): + def test_process_request_4XX_can_be_personalized(self) -> None: expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() - middleware = middlewares.RequestMiddleware(None) - - exception = Http404() - - def get_response(_response): - """Simulate an exception""" - middleware.process_exception(request, exception) + def get_response(_request: HttpRequest) -> HttpResponse: + try: + raise exception + except Exception: + got_request_exception.send(object, request=request) return HttpResponseNotFound() - middleware.get_response = get_response + middleware = RequestMiddleware(get_response) - with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( - logging.getLogger("django_structlog"), logging.INFO - ) as log_results: + exception = Http404() + + with ( + patch("uuid.UUID.__str__", return_value=expected_uuid), + self.assertLogs( + logging.getLogger("django_structlog"), logging.INFO + ) as log_results, + ): middleware(request) self.assertEqual(2, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) @@ -685,25 +723,27 @@ class TestRequestMiddleware(TestCase): self.assertNotIn("user_id", record.msg) self.assertFalse(hasattr(request, "_raised_exception")) - def test_process_request_500_are_processed_as_errors(self): + def test_process_request_500_are_processed_as_errors(self) -> None: expected_uuid = "00000000-0000-0000-0000-000000000000" request = self.factory.get("/foo") request.user = AnonymousUser() - middleware = middlewares.RequestMiddleware(None) - - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: return HttpResponseServerError() - middleware.get_response = get_response + middleware = RequestMiddleware(get_response) - with patch("uuid.UUID.__str__", return_value=expected_uuid), self.assertLogs( - logging.getLogger("django_structlog"), logging.INFO - ) as log_results: + with ( + patch("uuid.UUID.__str__", return_value=expected_uuid), + self.assertLogs( + logging.getLogger("django_structlog"), logging.INFO + ) as log_results, + ): middleware(request) self.assertEqual(2, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("request_id", record.msg) @@ -731,12 +771,12 @@ class TestRequestMiddleware(TestCase): self.assertNotIn("user_id", record.msg) self.assertFalse(hasattr(request, "_raised_exception")) - def test_should_log_request_id_from_request_x_request_id_header(self): + def test_should_log_request_id_from_request_x_request_id_header(self) -> None: mock_response = Mock() mock_response.status_code = 200 x_request_id = "my-fake-request-id" - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results @@ -744,10 +784,11 @@ class TestRequestMiddleware(TestCase): request = RequestFactory(HTTP_X_REQUEST_ID=x_request_id).get("/foo") - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) middleware(request) self.assertEqual(1, len(self.log_results.records)) + record: Any record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) @@ -755,12 +796,14 @@ class TestRequestMiddleware(TestCase): self.assertNotIn("user_id", record.msg) self.assertEqual(x_request_id, record.msg["request_id"]) - def test_should_log_correlation_id_from_request_x_correlation_id_header(self): + def test_should_log_correlation_id_from_request_x_correlation_id_header( + self, + ) -> None: mock_response = Mock() mock_response.status_code = 200 x_correlation_id = "my-fake-correlation-id" - def get_response(_response): + def get_response(_request: HttpRequest) -> HttpResponse: with self.assertLogs(__name__, logging.INFO) as log_results: self.logger.info("hello") self.log_results = log_results @@ -768,10 +811,11 @@ class TestRequestMiddleware(TestCase): request = RequestFactory(HTTP_X_CORRELATION_ID=x_correlation_id).get("/foo") - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) middleware(request) self.assertEqual(1, len(self.log_results.records)) + record: Any record = self.log_results.records[0] self.assertEqual("INFO", record.levelname) @@ -779,113 +823,125 @@ class TestRequestMiddleware(TestCase): self.assertNotIn("user_id", record.msg) self.assertEqual(x_correlation_id, record.msg["correlation_id"]) - def test_sync_streaming_response(self): - def streaming_content(): + def test_sync_streaming_response(self) -> None: + def streaming_content() -> Generator[Any, None, None]: # pragma: no cover yield mock_response = mock.create_autospec(StreamingHttpResponse) mock_response.streaming_content = streaming_content() + mock_response.is_async = False mock_response.status_code = 200 - def get_response(_response): + def get_response(_request: HttpRequest) -> Any: return mock_response request = RequestFactory().get("/foo") - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) mock_wrapper = AsyncMock() with patch( "django_structlog.middlewares.request.sync_streaming_content_wrapper", return_value=mock_wrapper, ) as mock_sync_streaming_response_wrapper: - response = middleware(request) + response: Any = middleware(request) mock_sync_streaming_response_wrapper.assert_called_once() self.assertEqual(response.streaming_content, mock_wrapper) - def test_async_streaming_response(self): - async def streaming_content(): + def test_async_streaming_response(self) -> None: + async def streaming_content() -> AsyncGenerator[Any, None]: # pragma: no cover yield mock_response = mock.create_autospec(StreamingHttpResponse) mock_response.streaming_content = streaming_content() + mock_response.is_async = True mock_response.status_code = 200 - def get_response(_response): + def get_response(_request: HttpRequest) -> Any: return mock_response request = RequestFactory().get("/foo") - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) mock_wrapper = AsyncMock() with patch( "django_structlog.middlewares.request.async_streaming_content_wrapper", return_value=mock_wrapper, ) as mock_sync_streaming_response_wrapper: - response = middleware(request) + response: Any = middleware(request) mock_sync_streaming_response_wrapper.assert_called_once() self.assertEqual(response.streaming_content, mock_wrapper) - async def test_async_cancel(self): - async def async_get_response(request): + async def test_async_cancel(self) -> None: + async def async_get_response(request: HttpRequest) -> Any: raise asyncio.CancelledError - middleware = middlewares.RequestMiddleware(async_get_response) + middleware = RequestMiddleware(async_get_response) mock_request = Mock() - with patch( - "django_structlog.middlewares.request.RequestMiddleware.prepare" - ) as mock_prepare, patch( - "django_structlog.middlewares.request.RequestMiddleware.handle_response" - ) as mock_handle_response, self.assertLogs( - "django_structlog.middlewares.request", logging.WARNING - ) as log_results: + with ( + patch( + "django_structlog.middlewares.RequestMiddleware.prepare" + ) as mock_prepare, + patch( + "django_structlog.middlewares.RequestMiddleware.handle_response" + ) as mock_handle_response, + self.assertLogs( + "django_structlog.middlewares.request", logging.WARNING + ) as log_results, + ): with self.assertRaises(asyncio.CancelledError): - await middleware(mock_request) + await cast(Awaitable[HttpResponse], middleware(mock_request)) mock_prepare.assert_called_once_with(mock_request) mock_handle_response.assert_not_called() self.assertEqual(1, len(log_results.records)) - record = log_results.records[0] + record: Any = log_results.records[0] self.assertEqual("request_cancelled", record.msg["event"]) class TestRequestMiddlewareRouter(TestCase): - async def test_async(self): + async def test_async(self) -> None: mock_response = Mock() - async def async_get_response(request): + async def async_get_response(request: HttpRequest) -> Any: return mock_response - middleware = middlewares.RequestMiddleware(async_get_response) + middleware = RequestMiddleware(async_get_response) mock_request = Mock() - with patch( - "django_structlog.middlewares.request.RequestMiddleware.prepare" - ) as mock_prepare, patch( - "django_structlog.middlewares.request.RequestMiddleware.handle_response" - ) as mock_handle_response: - response = await middleware(mock_request) + with ( + patch( + "django_structlog.middlewares.RequestMiddleware.prepare" + ) as mock_prepare, + patch( + "django_structlog.middlewares.RequestMiddleware.handle_response" + ) as mock_handle_response, + ): + response = await cast(Awaitable[HttpResponse], middleware(mock_request)) self.assertEqual(response, mock_response) mock_prepare.assert_called_once_with(mock_request) mock_handle_response.assert_called_once_with(mock_request, mock_response) - def test_sync(self): + def test_sync(self) -> None: mock_response = Mock() - def get_response(request): + def get_response(request: HttpRequest) -> HttpResponse: return mock_response - middleware = middlewares.RequestMiddleware(get_response) + middleware = RequestMiddleware(get_response) mock_request = Mock() - with patch( - "django_structlog.middlewares.request.RequestMiddleware.prepare" - ) as mock_prepare, patch( - "django_structlog.middlewares.request.RequestMiddleware.handle_response" - ) as mock_handle_response: + with ( + patch( + "django_structlog.middlewares.RequestMiddleware.prepare" + ) as mock_prepare, + patch( + "django_structlog.middlewares.RequestMiddleware.handle_response" + ) as mock_handle_response, + ): response = middleware(mock_request) self.assertEqual(response, mock_response) mock_prepare.assert_called_once_with(mock_request) @@ -893,39 +949,41 @@ class TestRequestMiddlewareRouter(TestCase): class TestGetRequestHeader(TestCase): - def test_django_22_or_higher(self): + def test_django_22_or_higher(self) -> None: mock_request = mock.MagicMock(spec=["headers"]) get_request_header(mock_request, "x-foo-bar", "HTTP_X_FOO_BAR") mock_request.headers.get.assert_called_once_with("x-foo-bar") - def test_django_prior_to_22(self): + def test_django_prior_to_22(self) -> None: mock_request = mock.MagicMock(spec=["META"]) get_request_header(mock_request, "x-foo-bar", "HTTP_X_FOO_BAR") mock_request.META.get.assert_called_once_with("HTTP_X_FOO_BAR") class TestSyncStreamingContentWrapper(TestCase): - def setUp(self): + def setUp(self) -> None: self.logger = structlog.getLogger(__name__) - def test_success(self): + def test_success(self) -> None: result = Mock() - def streaming_content(): + def streaming_content() -> Generator[Any, None, None]: self.logger.info("streaming_content") yield result wrapped_streaming_content = sync_streaming_content_wrapper( streaming_content(), {"foo": "bar"} ) - with self.assertLogs( - __name__, logging.INFO - ) as streaming_content_log_results, self.assertLogs( - "django_structlog.middlewares.request", logging.INFO - ) as log_results: + with ( + self.assertLogs(__name__, logging.INFO) as streaming_content_log_results, + self.assertLogs( + "django_structlog.middlewares.request", logging.INFO + ) as log_results, + ): self.assertEqual(result, next(wrapped_streaming_content)) self.assertEqual(1, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_started", record.msg["event"]) @@ -950,12 +1008,15 @@ class TestSyncStreamingContentWrapper(TestCase): self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) - def test_failure(self): + def test_failure(self) -> None: result = Mock() - exception = Exception() + class CustomException(Exception): + pass + + exception = CustomException() - def streaming_content(): + def streaming_content() -> Generator[Any, None, None]: self.logger.info("streaming_content") yield result raise exception @@ -963,14 +1024,16 @@ class TestSyncStreamingContentWrapper(TestCase): wrapped_streaming_content = sync_streaming_content_wrapper( streaming_content(), {"foo": "bar"} ) - with self.assertLogs( - __name__, logging.INFO - ) as streaming_content_log_results, self.assertLogs( - "django_structlog.middlewares.request", logging.INFO - ) as log_results: + with ( + self.assertLogs(__name__, logging.INFO) as streaming_content_log_results, + self.assertLogs( + "django_structlog.middlewares.request", logging.INFO + ) as log_results, + ): self.assertEqual(result, next(wrapped_streaming_content)) self.assertEqual(1, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_started", record.msg["event"]) @@ -980,7 +1043,7 @@ class TestSyncStreamingContentWrapper(TestCase): with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: - self.assertRaises(Exception, next, wrapped_streaming_content) + self.assertRaises(CustomException, next, wrapped_streaming_content) self.assertEqual(1, len(streaming_content_log_results.records)) record = streaming_content_log_results.records[0] @@ -997,27 +1060,29 @@ class TestSyncStreamingContentWrapper(TestCase): class TestASyncStreamingContentWrapper(TestCase): - def setUp(self): + def setUp(self) -> None: self.logger = structlog.getLogger(__name__) - async def test_success(self): + async def test_success(self) -> None: result = Mock() - async def streaming_content(): + async def streaming_content() -> AsyncGenerator[Any, None]: self.logger.info("streaming_content") yield result wrapped_streaming_content = async_streaming_content_wrapper( streaming_content(), {"foo": "bar"} ) - with self.assertLogs( - __name__, logging.INFO - ) as streaming_content_log_results, self.assertLogs( - "django_structlog.middlewares.request", logging.INFO - ) as log_results: + with ( + self.assertLogs(__name__, logging.INFO) as streaming_content_log_results, + self.assertLogs( + "django_structlog.middlewares.request", logging.INFO + ) as log_results, + ): self.assertEqual(result, await wrapped_streaming_content.__anext__()) self.assertEqual(1, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_started", record.msg["event"]) @@ -1043,12 +1108,15 @@ class TestASyncStreamingContentWrapper(TestCase): self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) - async def test_failure(self): + async def test_failure(self) -> None: result = Mock() - exception = Exception() + class CustomException(Exception): + pass - async def streaming_content(): + exception = CustomException() + + async def streaming_content() -> AsyncGenerator[Any, None]: self.logger.info("streaming_content") yield result raise exception @@ -1056,14 +1124,16 @@ class TestASyncStreamingContentWrapper(TestCase): wrapped_streaming_content = async_streaming_content_wrapper( streaming_content(), {"foo": "bar"} ) - with self.assertLogs( - __name__, logging.INFO - ) as streaming_content_log_results, self.assertLogs( - "django_structlog.middlewares.request", logging.INFO - ) as log_results: + with ( + self.assertLogs(__name__, logging.INFO) as streaming_content_log_results, + self.assertLogs( + "django_structlog.middlewares.request", logging.INFO + ) as log_results, + ): self.assertEqual(result, await wrapped_streaming_content.__anext__()) self.assertEqual(1, len(log_results.records)) + record: Any record = log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertEqual("streaming_started", record.msg["event"]) @@ -1073,7 +1143,7 @@ class TestASyncStreamingContentWrapper(TestCase): with self.assertLogs( "django_structlog.middlewares.request", logging.INFO ) as log_results: - with self.assertRaises(StopAsyncIteration): + with self.assertRaises(CustomException): await wrapped_streaming_content.__anext__() self.assertEqual(1, len(streaming_content_log_results.records)) @@ -1089,12 +1159,12 @@ class TestASyncStreamingContentWrapper(TestCase): self.assertIn("foo", record.msg) self.assertEqual("bar", record.msg["foo"]) - async def test_cancel(self): + async def test_cancel(self) -> None: result = Mock() exception = asyncio.CancelledError() - async def streaming_content(): + async def streaming_content() -> AsyncGenerator[Any, None]: self.logger.info("streaming_content") yield result raise exception @@ -1111,6 +1181,7 @@ class TestASyncStreamingContentWrapper(TestCase): await wrapped_streaming_content.__anext__() self.assertEqual(1, len(streaming_content_log_results.records)) + record: Any record = streaming_content_log_results.records[0] self.assertEqual("INFO", record.levelname) self.assertIn("foo", record.msg) diff --git a/test_app/tests/test_app_settings.py b/test_app/tests/test_app_settings.py index 08c83b62fbde7de5688d2b141f26d0253447ac63..5a8469fbe1a6d94031a566acbd4f10d3857f9474 100644 --- a/test_app/tests/test_app_settings.py +++ b/test_app/tests/test_app_settings.py @@ -4,13 +4,13 @@ from django_structlog import app_settings class TestAppSettings(TestCase): - def test_celery_enabled(self): + def test_celery_enabled(self) -> None: settings = app_settings.AppSettings() with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=True): self.assertTrue(settings.CELERY_ENABLED) - def test_celery_disabled(self): + def test_celery_disabled(self) -> None: settings = app_settings.AppSettings() with self.settings(DJANGO_STRUCTLOG_CELERY_ENABLED=False): diff --git a/test_app/tests/test_apps.py b/test_app/tests/test_apps.py index 5efbe8bc8792c70c85dc21e0ddddeee6cce76da6..bc985b80aac76aa54f1c10838890c5f1383d1c92 100644 --- a/test_app/tests/test_apps.py +++ b/test_app/tests/test_apps.py @@ -1,4 +1,4 @@ -from unittest.mock import patch, create_autospec +from unittest.mock import create_autospec, patch from django.test import TestCase @@ -7,7 +7,7 @@ from django_structlog.celery import receivers class TestAppConfig(TestCase): - def test_celery_enabled(self): + def test_celery_enabled(self) -> None: app = apps.DjangoStructLogConfig( "django_structlog", __import__("django_structlog") ) @@ -23,7 +23,7 @@ class TestAppConfig(TestCase): self.assertTrue(hasattr(app, "_celery_receiver")) self.assertIsNotNone(app._celery_receiver) - def test_celery_disabled(self): + def test_celery_disabled(self) -> None: app = apps.DjangoStructLogConfig( "django_structlog", __import__("django_structlog") ) @@ -39,7 +39,7 @@ class TestAppConfig(TestCase): self.assertFalse(hasattr(app, "_celery_receiver")) - def test_command_enabled(self): + def test_command_enabled(self) -> None: app = apps.DjangoStructLogConfig( "django_structlog", __import__("django_structlog") ) @@ -55,7 +55,7 @@ class TestAppConfig(TestCase): self.assertTrue(hasattr(app, "_django_command_receiver")) self.assertIsNotNone(app._django_command_receiver) - def test_command_disabled(self): + def test_command_disabled(self) -> None: app = apps.DjangoStructLogConfig( "django_structlog", __import__("django_structlog") ) diff --git a/test_app/tests/test_commands.py b/test_app/tests/test_commands.py index c99ac96abcc862f1d4aabc444664cd15e3f144d8..5fca0a9521fcc2207ee88c1a8f75c53778ef09bc 100644 --- a/test_app/tests/test_commands.py +++ b/test_app/tests/test_commands.py @@ -1,26 +1,32 @@ import logging +from typing import Any import structlog -from django.test import TestCase from django.core.management import BaseCommand, call_command -from django_extensions.management.utils import signalcommand +from django.test import TestCase +from django_extensions.management.utils import ( # type: ignore[import-untyped] + signalcommand, +) class TestCommands(TestCase): - def test_command(self): + def test_command(self) -> None: class Command(BaseCommand): - @signalcommand - def handle(self, *args, **options): + + @signalcommand # type: ignore[misc] + def handle(self, *args: Any, **options: Any) -> Any: structlog.getLogger("command").info("command_event") - with self.assertLogs( - "command", logging.INFO - ) as command_log_results, self.assertLogs( - "django_structlog.commands", logging.INFO - ) as django_structlog_commands_log_results: + with ( + self.assertLogs("command", logging.INFO) as command_log_results, + self.assertLogs( + "django_structlog.commands", logging.INFO + ) as django_structlog_commands_log_results, + ): call_command(Command()) self.assertEqual(1, len(command_log_results.records)) + record: Any record = command_log_results.records[0] self.assertEqual("command_event", record.msg["event"]) self.assertIn("command_id", record.msg) @@ -33,34 +39,34 @@ class TestCommands(TestCase): self.assertEqual("command_finished", record.msg["event"]) self.assertIn("command_id", record.msg) - def test_nested_command(self): + def test_nested_command(self) -> None: class Command(BaseCommand): - @signalcommand - def handle(self, *args, **options): + @signalcommand # type: ignore[misc] + def handle(self, *args: Any, **options: Any) -> None: logger = structlog.getLogger("command") logger.info("command_event_1") call_command(NestedCommand()) logger.info("command_event_2") class NestedCommand(BaseCommand): - @signalcommand - def handle(self, *args, **options): + @signalcommand # type: ignore[misc] + def handle(self, *args: Any, **options: Any) -> None: structlog.getLogger("nested_command").info("nested_command_event") - with self.assertLogs( - "command", logging.INFO - ) as command_log_results, self.assertLogs( - "nested_command", logging.INFO - ), self.assertLogs( - "django_structlog.commands", logging.INFO - ) as django_structlog_commands_log_results: + with ( + self.assertLogs("command", logging.INFO) as command_log_results, + self.assertLogs("nested_command", logging.INFO), + self.assertLogs( + "django_structlog.commands", logging.INFO + ) as django_structlog_commands_log_results, + ): call_command(Command()) self.assertEqual(2, len(command_log_results.records)) - command_event_1 = command_log_results.records[0] + command_event_1: Any = command_log_results.records[0] self.assertEqual("command_event_1", command_event_1.msg["event"]) self.assertIn("command_id", command_event_1.msg) - command_event_2 = command_log_results.records[1] + command_event_2: Any = command_log_results.records[1] self.assertEqual("command_event_2", command_event_2.msg["event"]) self.assertIn("command_id", command_event_2.msg) self.assertEqual( @@ -68,11 +74,11 @@ class TestCommands(TestCase): ) self.assertEqual(4, len(django_structlog_commands_log_results.records)) - command_started_1 = django_structlog_commands_log_results.records[0] + command_started_1: Any = django_structlog_commands_log_results.records[0] self.assertEqual("command_started", command_started_1.msg["event"]) self.assertIn("command_id", command_started_1.msg) - command_started_2 = django_structlog_commands_log_results.records[1] + command_started_2: Any = django_structlog_commands_log_results.records[1] self.assertEqual("command_started", command_started_2.msg["event"]) self.assertIn("command_id", command_started_2.msg) self.assertIn("parent_command_id", command_started_2.msg) @@ -81,7 +87,7 @@ class TestCommands(TestCase): command_started_2.msg["parent_command_id"], ) - command_finished_1 = django_structlog_commands_log_results.records[2] + command_finished_1: Any = django_structlog_commands_log_results.records[2] self.assertEqual("command_finished", command_finished_1.msg["event"]) self.assertIn("command_id", command_finished_1.msg) self.assertIn("parent_command_id", command_finished_1.msg) @@ -90,7 +96,7 @@ class TestCommands(TestCase): command_finished_1.msg["parent_command_id"], ) - command_finished_2 = django_structlog_commands_log_results.records[3] + command_finished_2: Any = django_structlog_commands_log_results.records[3] self.assertEqual("command_finished", command_finished_2.msg["event"]) self.assertIn("command_id", command_finished_2.msg) self.assertNotIn("parent_command_id", command_finished_2.msg)