From c88747eb56456ac3b60aa49e198980efd09d7e6a Mon Sep 17 00:00:00 2001 From: MD Abdur Rahim Date: Fri, 4 Sep 2026 04:13:49 +0600 Subject: [PATCH 1/2] Fix ViewInspector sharing view state across views (#6877) A single ViewInspector instance (e.g. an AutoSchema assigned via @action(schema=...)) can end up attached to multiple views when the action is defined on a mixin reused by several ViewSets. Since `view` was stored as a plain mutable attribute on the shared instance, each view that touched it would overwrite `.view` for every other view sharing it, causing schema generation to collapse onto whichever view set it last. __set__ now copies the schema instance per view before binding `view` to it, so each view gets its own independent copy instead of sharing mutable state. --- rest_framework/schemas/inspectors.py | 9 ++++- tests/schemas/test_view_inspector.py | 49 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 tests/schemas/test_view_inspector.py diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py index e027b46a70..56a857394e 100644 --- a/rest_framework/schemas/inspectors.py +++ b/rest_framework/schemas/inspectors.py @@ -3,6 +3,7 @@ See schemas.__init__.py for package overview. """ +import copy import re from weakref import WeakKeyDictionary @@ -48,9 +49,15 @@ def __get__(self, instance, owner): return self def __set__(self, instance, other): - self.instance_schemas[instance] = other if other is not None: + # A single `ViewInspector` instance may be shared by multiple + # views, e.g. when `@action(schema=AutoSchema())` is used on a + # method defined in a mixin and reused across several ViewSets. + # Store an independent copy per view so that `other.view` isn't + # silently overwritten by whichever view is set last. + other = copy.copy(other) other.view = instance + self.instance_schemas[instance] = other @property def view(self): diff --git a/tests/schemas/test_view_inspector.py b/tests/schemas/test_view_inspector.py new file mode 100644 index 0000000000..84b14162d1 --- /dev/null +++ b/tests/schemas/test_view_inspector.py @@ -0,0 +1,49 @@ +from django.test import TestCase + +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.routers import DefaultRouter +from rest_framework.schemas.openapi import AutoSchema, SchemaGenerator +from rest_framework.serializers import Serializer +from rest_framework.viewsets import GenericViewSet + + +class TestViewInspectorDescriptor(TestCase): + """ + Regression tests for #6877: a `ViewInspector` (e.g. `AutoSchema`) + instance shared across multiple views, such as one assigned via + `@action(schema=AutoSchema())` on a method defined in a mixin, must + not leak the `view` it was last accessed with onto other views. + """ + def test_schema_shared_via_mixin_action_is_not_shared_between_viewsets(self): + shared_schema = AutoSchema() + + class CancelViewSetMixin: + @action(methods=['post'], detail=True, schema=shared_schema) + def cancel(self, request, pk): + return Response() + + class ASerializer(Serializer): + pass + + class BSerializer(Serializer): + pass + + class ViewSetA(CancelViewSetMixin, GenericViewSet): + serializer_class = ASerializer + + class ViewSetB(CancelViewSetMixin, GenericViewSet): + serializer_class = BSerializer + + router = DefaultRouter() + router.register(r'view-set-a', ViewSetA, basename='viewseta') + router.register(r'view-set-b', ViewSetB, basename='viewsetb') + + generator = SchemaGenerator(title='Test', patterns=router.urls) + schema = generator.get_schema(request=None, public=True) + + operation_id_a = schema['paths']['/view-set-a/{id}/cancel/']['post']['operationId'] + operation_id_b = schema['paths']['/view-set-b/{id}/cancel/']['post']['operationId'] + + assert operation_id_a == 'cancelA' + assert operation_id_b == 'cancelB' From 5986e033036e749980df60cf680ae854c6ad3fd0 Mon Sep 17 00:00:00 2001 From: MD Abdur Rahim Date: Fri, 4 Sep 2026 04:20:31 +0600 Subject: [PATCH 2/2] Skip schema regression test when uritemplate is not installed The base tox environment doesn't install the optional uritemplate dependency, and SchemaGenerator.get_schema() asserts on it while computing path parameters. Guard the new test the same way the rest of tests/schemas/test_openapi.py already does. --- tests/schemas/test_view_inspector.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/schemas/test_view_inspector.py b/tests/schemas/test_view_inspector.py index 84b14162d1..7ef0cf42de 100644 --- a/tests/schemas/test_view_inspector.py +++ b/tests/schemas/test_view_inspector.py @@ -1,5 +1,7 @@ +import pytest from django.test import TestCase +from rest_framework.compat import uritemplate from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.routers import DefaultRouter @@ -15,6 +17,7 @@ class TestViewInspectorDescriptor(TestCase): `@action(schema=AutoSchema())` on a method defined in a mixin, must not leak the `view` it was last accessed with onto other views. """ + @pytest.mark.skipif(uritemplate is None, reason='uritemplate not installed.') def test_schema_shared_via_mixin_action_is_not_shared_between_viewsets(self): shared_schema = AutoSchema()