diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 1a8bd25..73afd47 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -63,6 +63,7 @@ jobs: - djangorestframework django - djangorestframework django==4.2.* - djangorestframework==3.12.* django==3.2.* + - djangorestframework==3.10.* django==2.2.* - django-ninja django - django-ninja==0.22.* django - django-ninja==0.18.0 django diff --git a/apitally/django.py b/apitally/django.py index cb3b4db..32fbf7e 100644 --- a/apitally/django.py +++ b/apitally/django.py @@ -10,7 +10,7 @@ from django.conf import settings from django.core.exceptions import ViewDoesNotExist from django.test import RequestFactory -from django.urls import URLPattern, URLResolver, get_resolver, resolve +from django.urls import Resolver404, URLPattern, URLResolver, get_resolver, resolve from django.utils.module_loading import import_string from apitally.client.threading import ApitallyClient @@ -41,12 +41,15 @@ def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None: config = getattr(settings, "APITALLY_MIDDLEWARE", {}) self.configure(**config) assert self.config is not None - self.views = _extract_views_from_url_patterns(get_resolver().url_patterns) + views = _extract_views_from_url_patterns(get_resolver().url_patterns) + self.view_lookup = { + view.pattern: view for view in reversed(_extract_views_from_url_patterns(get_resolver().url_patterns)) + } self.client = ApitallyClient(client_id=self.config.client_id, env=self.config.env) self.client.start_sync_loop() self.client.set_app_info( app_info=_get_app_info( - views=self.views, + views=views, app_version=self.config.app_version, openapi_url=self.config.openapi_url, ) @@ -84,7 +87,7 @@ def __call__(self, request: HttpRequest) -> HttpResponse: status_code=response.status_code, response_time=time.perf_counter() - start_time, request_size=request.headers.get("Content-Length"), - response_size=response.headers.get("Content-Length") # type: ignore[attr-defined] + response_size=response["Content-Length"] if response.has_header("Content-Length") else (len(response.content) if not response.streaming else None), ) @@ -108,8 +111,11 @@ def __call__(self, request: HttpRequest) -> HttpResponse: return response def get_view(self, request: HttpRequest) -> Optional[DjangoViewInfo]: - resolver_match = resolve(request.path_info) - return next((view for view in self.views if view.pattern == resolver_match.route), None) + try: + resolver_match = resolve(request.path_info) + return self.view_lookup.get(resolver_match.route) + except Resolver404: # pragma: no cover + return None def get_consumer(self, request: HttpRequest) -> Optional[str]: if hasattr(request, "consumer_identifier"): @@ -165,8 +171,7 @@ def _get_app_info( app_info: Dict[str, Any] = {} if openapi := _get_openapi(views, openapi_url): app_info["openapi"] = openapi - if paths := _get_paths(views): - app_info["paths"] = paths + app_info["paths"] = _get_paths(views) app_info["versions"] = _get_versions(app_version) app_info["client"] = "python:django" return app_info @@ -255,7 +260,7 @@ def _get_versions(app_version: Optional[str]) -> Dict[str, str]: "django": version("django"), } try: - versions["django-rest-framework"] = version("django-rest-framework") + versions["djangorestframework"] = version("djangorestframework") except PackageNotFoundError: # pragma: no cover pass try: diff --git a/poetry.lock b/poetry.lock index e499a58..4492619 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2443,4 +2443,4 @@ starlette = ["httpx", "starlette"] [metadata] lock-version = "2.0" python-versions = ">=3.8,<4.0" -content-hash = "16fa2d5d2a8ed1b3ff26a5696823a83933acd7ba893d696f76db18c1dcea344b" +content-hash = "cdf7fa20ad6a3fd43c4a96b9db5d21261d94382d8f6bb4a569f78d233e12eb45" diff --git a/pyproject.toml b/pyproject.toml index 09907b0..1adddff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,9 +28,9 @@ backoff = ">=2.0.0" python = ">=3.8,<4.0" # Optional dependencies, included in extras -django = { version = ">=4.0", optional = true } +django = { version = ">=2.2", optional = true } django-ninja = { version = ">=0.18.0", optional = true } -djangorestframework = { version = ">=3.12.0", optional = true } +djangorestframework = { version = ">=3.10.0", optional = true } fastapi = { version = ">=0.87.0", optional = true } flask = { version = ">=2.0.0", optional = true } httpx = { version = ">=0.22.0", optional = true } diff --git a/tests/test_django_ninja.py b/tests/test_django_ninja.py index cde1699..f1d9681 100644 --- a/tests/test_django_ninja.py +++ b/tests/test_django_ninja.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import sys from importlib.util import find_spec from typing import TYPE_CHECKING, Optional @@ -22,23 +23,23 @@ def identify_consumer(request: HttpRequest) -> Optional[str]: return None +@pytest.fixture(scope="module") +def reset_modules() -> None: + for module in list(sys.modules): + if module.startswith("django.") or module.startswith("apitally."): + del sys.modules[module] + + @pytest.fixture(scope="module", autouse=True) -def setup(module_mocker: MockerFixture) -> None: +def setup(reset_modules, module_mocker: MockerFixture) -> None: import django - from django.apps.registry import apps from django.conf import settings - from django.utils.functional import empty module_mocker.patch("apitally.client.threading.ApitallyClient._instance", None) module_mocker.patch("apitally.client.threading.ApitallyClient.start_sync_loop") module_mocker.patch("apitally.client.threading.ApitallyClient.set_app_info") module_mocker.patch("apitally.django.ApitallyMiddleware.config", None) - settings._wrapped = empty - apps.app_configs.clear() - apps.loading = False - apps.ready = False - settings.configure( ROOT_URLCONF="tests.django_ninja_urls", ALLOWED_HOSTS=["testserver"], @@ -57,9 +58,12 @@ def setup(module_mocker: MockerFixture) -> None: @pytest.fixture(scope="module") -def client() -> Client: +def client(module_mocker: MockerFixture) -> Client: + import django from django.test import Client + if django.VERSION[0] < 3: + module_mocker.patch("django.test.client.Client.store_exc_info") # Simulate raise_request_exception=False return Client(raise_request_exception=False) diff --git a/tests/test_django_rest_framework.py b/tests/test_django_rest_framework.py index 2cf89ca..d9ceefe 100644 --- a/tests/test_django_rest_framework.py +++ b/tests/test_django_rest_framework.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from importlib.util import find_spec from typing import TYPE_CHECKING @@ -14,23 +15,23 @@ from rest_framework.test import APIClient +@pytest.fixture(scope="module") +def reset_modules() -> None: + for module in list(sys.modules): + if module.startswith("django.") or module.startswith("rest_framework.") or module.startswith("apitally."): + del sys.modules[module] + + @pytest.fixture(scope="module", autouse=True) -def setup(module_mocker: MockerFixture) -> None: +def setup(reset_modules, module_mocker: MockerFixture) -> None: import django - from django.apps.registry import apps from django.conf import settings - from django.utils.functional import empty module_mocker.patch("apitally.client.threading.ApitallyClient._instance", None) module_mocker.patch("apitally.client.threading.ApitallyClient.start_sync_loop") module_mocker.patch("apitally.client.threading.ApitallyClient.set_app_info") module_mocker.patch("apitally.django.ApitallyMiddleware.config", None) - settings._wrapped = empty - apps.app_configs.clear() - apps.loading = False - apps.ready = False - settings.configure( ROOT_URLCONF="tests.django_rest_framework_urls", ALLOWED_HOSTS=["testserver"], @@ -53,9 +54,12 @@ def setup(module_mocker: MockerFixture) -> None: @pytest.fixture(scope="module") -def client() -> APIClient: +def client(module_mocker: MockerFixture) -> APIClient: + import django from rest_framework.test import APIClient + if django.VERSION[0] < 3: + module_mocker.patch("django.test.client.Client.store_exc_info") # Simulate raise_request_exception=False return APIClient(raise_request_exception=False)