From 2d001180a6992d2ce18723b3fe510979dff6472d Mon Sep 17 00:00:00 2001 From: ori roza Date: Sun, 12 May 2024 13:12:18 +0300 Subject: [PATCH 1/4] general changes: * renaming plugin to api-action * removing unnecessary code * updating docs --- README.md | 30 +++++++++++++++--------------- drf_api_action/plugin.py | 8 ++++---- drf_api_action/utils.py | 15 ++++++++------- pyproject.toml | 4 ++-- tests/tests.py | 26 +++++++++++++------------- 5 files changed, 42 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 5c125a7..69ad3b8 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ The drf-api-action Python package is designed to elevate your testing experience for Django Rest Framework (DRF) REST endpoints. -With the action_api fixture, this package empowers you to effortlessly test your REST endpoints as if they were conventional functions. +With the api_action fixture, this package empowers you to effortlessly test your REST endpoints as if they were conventional functions. Features: -* **Simplified Testing:** Testing DRF REST endpoints using the action_api plugin, treating them like regular functions. +* **Simplified Testing:** Testing DRF REST endpoints using the api_action plugin, treating them like regular functions. * **Seamless Integration:** you don't need to do anything in existing server code. @@ -41,7 +41,7 @@ from tests.test_server.test_app.views import DummyViewSetFixture #### Step 2: use the following action_api mark decorator: -`@pytest.mark.action_api(view_set_class={YOUR VIEW_SET})` +`@pytest.mark.api_action(view_set_class={YOUR VIEW_SET})` e.g: our ViewSet is called `DummyViewSetFixture` @@ -51,8 +51,8 @@ import pytest from tests.test_server.test_app.views import DummyViewSetFixture -@pytest.mark.action_api(view_set_class=DummyViewSetFixture) -def test_call_as_api_fixture(db, action_api): +@pytest.mark.api_action(view_set_class=DummyViewSetFixture) +def test_call_as_api_fixture(db, api_action): pass ``` Now you can use all `DummyViewSetFixture` functionality! @@ -68,12 +68,12 @@ from tests.test_server.test_app.models import DummyModel from tests.test_server.test_app.views import DummyViewSetFixture -@pytest.mark.action_api(view_set_class=DummyViewSetFixture) -def test_call_as_api_fixture(db, action_api): +@pytest.mark.api_action(view_set_class=DummyViewSetFixture) +def test_call_as_api_fixture(db, api_action): dummy_model = DummyModel() dummy_model.dummy_int = 1 dummy_model.save() - res = action_api.api_dummy(pk=1) + res = api_action.api_dummy(pk=1) assert res["dummy_int"] == 1 ``` @@ -84,9 +84,9 @@ import pytest from tests.test_server.test_app.views import DummyViewSetFixture -@pytest.mark.action_api(view_set_class=DummyViewSetFixture) -def test_dummy(db, action_api): - result = action_api.dummy(pk='bbb') +@pytest.mark.api_action(view_set_class=DummyViewSetFixture) +def test_dummy(db, api_action): + result = api_action.dummy(pk='bbb') assert result['dummy_int'] == 1 ``` @@ -125,21 +125,21 @@ filter_kwargs = {'pk': 'bb'} Call endpoints with pagination: ```python -@pytest.mark.action_api(view_set_class=DummyAPIViewSet) -def test_pagination_data(db, action_api): +@pytest.mark.api_action(view_set_class=DummyAPIViewSet) +def test_pagination_data(db, api_action): for i in range(1, 3): dummy_model = DummyModel() dummy_model.dummy_int = 1 dummy_model.save() - response = action_api.by_dummy_int(dummy_int=1, page=1) + response = api_action.by_dummy_int(dummy_int=1, page=1) obj = response['results'][0] assert obj['dummy_int'] == 1 assert extract_page_number(response['next']) == 2 - response = action_api.by_dummy_int(dummy_int=1, page=2) + response = api_action.by_dummy_int(dummy_int=1, page=2) assert extract_page_number(response['previous']) == 1 assert extract_page_number(response['next']) is None diff --git a/drf_api_action/plugin.py b/drf_api_action/plugin.py index 3b35877..b20e520 100644 --- a/drf_api_action/plugin.py +++ b/drf_api_action/plugin.py @@ -13,16 +13,16 @@ def api_item(*args, **kwargs): @pytest.fixture -def action_api(request): +def api_action(request): """ Make Dango WebView endpoints accessible """ from drf_api_action.mixins import APIRestMixin # pylint: disable=import-outside-toplevel - if request.keywords['action_api'].kwargs.get("view_set_class") is None: - raise ActionsAPIException('using action_api fixture must require a view_set_class kwarg') + if request.keywords['api_action'].kwargs.get("view_set_class") is None: + raise ActionsAPIException('using api_action fixture must require a view_set_class kwarg') - view_set_class = request.keywords['action_api'].kwargs["view_set_class"] + view_set_class = request.keywords['api_action'].kwargs["view_set_class"] class WrapperApiClass(APIRestMixin, view_set_class): def __getattribute__(self, item): diff --git a/drf_api_action/utils.py b/drf_api_action/utils.py index 1be8f3e..7292d6e 100644 --- a/drf_api_action/utils.py +++ b/drf_api_action/utils.py @@ -12,28 +12,29 @@ def __init__(self, data, query_params): self.query_params = query_params def build_absolute_uri(self, _=None): + """ + mocking django/http/request.py::HTTPRequest::build_absolute_uri + It's irrelevant since we do not provide any web resource + """ return '' def run_as_api(self, func, serializer_class, *args, **kw): + # adding to the view the request & kwargs including the serializer class kw.update({"serializer_class": serializer_class}) request = CustomRequest(kw, kw) self.kwargs = kw self.request = request try: - if hasattr(func, 'detail'): - # called from pytest fixture - ret = func(request, **kw) - else: - # called straight from viewer - ret = func(self, request, **kw) + ret = func(request, **kw) if isinstance(ret.data, list): # multiple results results = [dict(res) for res in ret.data] - else: + else: # only one json results = {k.lower(): v for k, v in ret.data.items()} except Exception as error: # pylint: disable=broad-except error_type = type(error) + # re-constructing the error with the actual traceback raised_exception = ActionsAPIExceptionMiddleware(*error.args, error_type=error_type, traceback=error.__traceback__) # fixing stack frames diff --git a/pyproject.toml b/pyproject.toml index 11c2556..2669fc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "drf_api_action" -version = "1.2.0" +version = "1.2.1" description = "drf-api-action elevates DRF testing by simplifying REST endpoint testing to a seamless, function-like experience." readme = "README.md" authors = ["Ori Roza "] @@ -41,7 +41,7 @@ classifiers = [ DJANGO_SETTINGS_MODULE = "tests.test_server.settings" python_files = "tests.py test_*.py *_tests.py" markers = [ - "action_api: Viewset" + "api_action: Viewset" ] testpaths = ["tests"] diff --git a/tests/tests.py b/tests/tests.py index 0d03b98..2e32c12 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -6,46 +6,46 @@ from tests.test_server.test_app.views import DummyViewSetFixture, DummyAPIViewSet -@pytest.mark.action_api(view_set_class=DummyViewSetFixture) -def test_call_as_api_fixture(db, action_api): +@pytest.mark.api_action(view_set_class=DummyViewSetFixture) +def test_call_as_api_fixture(db, api_action): dummy_model = DummyModel() dummy_model.dummy_int = 1 dummy_model.save() - res = action_api.api_dummy(pk=1) + res = api_action.api_dummy(pk=1) assert res["dummy_int"] == 1 -@pytest.mark.action_api(view_set_class=DummyAPIViewSet) -def test_pagination(db, action_api): +@pytest.mark.api_action(view_set_class=DummyAPIViewSet) +def test_pagination(db, api_action): dummy_model = DummyModel() dummy_model.dummy_int = 1 dummy_model.save() - obj = action_api.by_dummy_int(dummy_int=1)["results"][0] + obj = api_action.by_dummy_int(dummy_int=1)["results"][0] assert obj['dummy_int'] == 1 -@pytest.mark.action_api(view_set_class=DummyAPIViewSet) -def test_pagination_data(db, action_api): +@pytest.mark.api_action(view_set_class=DummyAPIViewSet) +def test_pagination_data(db, api_action): for i in range(1, 3): dummy_model = DummyModel() dummy_model.dummy_int = 1 dummy_model.save() - response = action_api.by_dummy_int(dummy_int=1) + response = api_action.by_dummy_int(dummy_int=1) assert extract_page_number(response['next']) == 2 obj = response['results'][0] assert obj['dummy_int'] == 1 - response = action_api.by_dummy_int(dummy_int=1, page=2) + response = api_action.by_dummy_int(dummy_int=1, page=2) assert extract_page_number(response['previous']) == 1 assert extract_page_number(response['next']) is None -@pytest.mark.action_api(view_set_class=DummyAPIViewSet) -def test_exceptions(db, action_api): +@pytest.mark.api_action(view_set_class=DummyAPIViewSet) +def test_exceptions(db, api_action): dummy_model = DummyModel() dummy_model.dummy_int = 1 dummy_model.save() with pytest.raises(ValidationError): - _ = action_api.by_dummy_int(dummy_int=-1) + _ = api_action.by_dummy_int(dummy_int=-1) From bae5dcc54560252881289e1f2ccb7baadcbe78b6 Mon Sep 17 00:00:00 2001 From: ori roza Date: Sun, 12 May 2024 13:13:11 +0300 Subject: [PATCH 2/4] general changes: * updating docs --- drf_api_action/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/drf_api_action/plugin.py b/drf_api_action/plugin.py index b20e520..842ce31 100644 --- a/drf_api_action/plugin.py +++ b/drf_api_action/plugin.py @@ -28,6 +28,7 @@ class WrapperApiClass(APIRestMixin, view_set_class): def __getattribute__(self, item): class_attribute = super().__getattribute__(item) + # running our logic on endpoints only if callable(class_attribute) and hasattr(class_attribute, 'detail'): return run_function(self, class_attribute) From 321a7b7886b0d408df2d575f5e22704356cade3a Mon Sep 17 00:00:00 2001 From: ori roza Date: Sun, 12 May 2024 13:36:48 +0300 Subject: [PATCH 3/4] general changes: * updating docs --- drf_api_action/plugin.py | 1 + drf_api_action/utils.py | 7 ++++--- tests/test_server/test_app/views.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/drf_api_action/plugin.py b/drf_api_action/plugin.py index 842ce31..88d7c2b 100644 --- a/drf_api_action/plugin.py +++ b/drf_api_action/plugin.py @@ -6,6 +6,7 @@ def run_function(self, func): def api_item(*args, **kwargs): + # here we retrieve serializer_class from @action decorator in order to inject it to the ViewSet serializer_class = func.kwargs['serializer_class'] return run_as_api(self, func, serializer_class, *args, **kwargs) diff --git a/drf_api_action/utils.py b/drf_api_action/utils.py index 7292d6e..366d74e 100644 --- a/drf_api_action/utils.py +++ b/drf_api_action/utils.py @@ -21,10 +21,11 @@ def build_absolute_uri(self, _=None): def run_as_api(self, func, serializer_class, *args, **kw): # adding to the view the request & kwargs including the serializer class - kw.update({"serializer_class": serializer_class}) + kw.update({"serializer_class": serializer_class}) # adding serializer class from @action + # decorator into our instance request = CustomRequest(kw, kw) - self.kwargs = kw - self.request = request + self.kwargs = kw # adding our enhanced kwargs into instance kwargs + self.request = request # mocking request with our arguments as data in the instance try: ret = func(request, **kw) diff --git a/tests/test_server/test_app/views.py b/tests/test_server/test_app/views.py index e4fcf29..d6212a9 100644 --- a/tests/test_server/test_app/views.py +++ b/tests/test_server/test_app/views.py @@ -58,6 +58,7 @@ def api_dummy(self, request, **kwargs): @action(detail=False, methods=["post"], serializer_class=GetDummyByIntSerializer) def by_dummy_int(self, request, **kwargs): + breakpoint() self.get_serializer(data=request.data).is_valid(raise_exception=True) queryset = DummyModel.objects.filter(dummy_int=request.data["dummy_int"]).order_by("id") page = self.paginate_queryset(queryset) From a0039754cb7aafe177dd52d93d479fe3d4cb6b4a Mon Sep 17 00:00:00 2001 From: ori roza Date: Sun, 12 May 2024 13:38:09 +0300 Subject: [PATCH 4/4] general changes: * lint --- tests/test_server/test_app/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_server/test_app/views.py b/tests/test_server/test_app/views.py index d6212a9..e4fcf29 100644 --- a/tests/test_server/test_app/views.py +++ b/tests/test_server/test_app/views.py @@ -58,7 +58,6 @@ def api_dummy(self, request, **kwargs): @action(detail=False, methods=["post"], serializer_class=GetDummyByIntSerializer) def by_dummy_int(self, request, **kwargs): - breakpoint() self.get_serializer(data=request.data).is_valid(raise_exception=True) queryset = DummyModel.objects.filter(dummy_int=request.data["dummy_int"]).order_by("id") page = self.paginate_queryset(queryset)