Skip to content

Added typing to package code. - #102

Open
noamkush wants to merge 1 commit into
vintasoftware:mainfrom
noamkush:feat/mypy
Open

Added typing to package code.#102
noamkush wants to merge 1 commit into
vintasoftware:mainfrom
noamkush:feat/mypy

Conversation

@noamkush

@noamkush noamkush commented Oct 27, 2025

Copy link
Copy Markdown

Description: This adds annotations to the code in drf_rw_serializers/

Dependencies: Fixes #58

Merge checklist:

  • All reviewers approved
  • CI build is green
  • Version bumped
  • Changelog record added
  • Documentation updated (not only docstrings)
  • Commits are squashed
  • PR author is listed in AUTHORS

Post merge:

  • Create a tag
  • Check new version is pushed to PyPi after tag-triggered build is
    finished.
  • Delete working branch (if not needed anymore)

Author concerns: This does break the current behavior of get_serializer_class as it may no longer return a None in the rare case where there's no request and no serializer configured. This is more compatible with rest framework, as the base implementation doesn't allow None to be returned and get_serializer assumes it didn't get a None.
Additionally, I had to bump the minimal python version and some package versions as it was very hard to keep Python 3.8 support.

Summary by Sourcery

Add comprehensive typing support and update project configuration for newer Python and dependency versions

Enhancements:

  • Add Python type hints to generics, mixins, and viewsets, including Request, Response, and serializer type annotations
  • Introduce a covariant Model TypeVar for GenericAPIView and enforce non-None get_serializer_class behavior
  • Add strict mypy configuration in pyproject.toml and enable mypy checks in prospector

Build:

  • Bump minimum Python to 3.9, update poetry dependencies with django-stubs, djangorestframework-stubs, pre-commit, and types-setuptools
  • Update setup.py classifiers to include Python 3.13 and Django 5.1/5.2 support

CI:

  • Refresh tox.ini and GitHub Actions matrices to drop Python 3.8, add Python 3.13, and include DRF 3.16 and Django 5.1/5.2 environments

Documentation:

  • Add type annotation to Sphinx conf.py and example_app URL patterns type definitions

Tests:

  • Modify test to expect AssertionError instead of None for get_serializer_class when no serializer is configured

@sourcery-ai

sourcery-ai Bot commented Oct 27, 2025

Copy link
Copy Markdown

Reviewer's Guide

This PR applies comprehensive type annotations across the generic views, mixins, and viewsets, upgrades the minimum Python and dependency versions, integrates strict MyPy configuration, and updates CI/CD and tests to match the new behavior.

Class diagram for updated generic views and mixins with type annotations

classDiagram
    class GenericAPIView {
        read_serializer_class: type[BaseSerializer[_MT_co]] | None
        write_serializer_class: type[BaseSerializer[_MT_co]] | None
        get_serializer_class() type[BaseSerializer[_MT_co]]
        get_read_serializer(*args: Any, **kwargs: Any) BaseSerializer[_MT_co]
        get_read_serializer_class(default_to_serializer_class: bool = False) type[BaseSerializer[_MT_co]]
        get_write_serializer(*args: Any, **kwargs: Any) BaseSerializer[_MT_co]
        get_write_serializer_class(default_to_serializer_class: bool = False) type[BaseSerializer[_MT_co]]
    }
    class CreateAPIView {
        post(request: Request, *args: Any, **kwargs: Any) Response
    }
    class UpdateAPIView {
        put(request: Request, *args: Any, **kwargs: Any) Response
        patch(request: Request, *args: Any, **kwargs: Any) Response
    }
    class ListAPIView {
        get(request: Request, *args: Any, **kwargs: Any) Response
    }
    class RetrieveAPIView {
        get(request: Request, *args: Any, **kwargs: Any) Response
    }
    class ListCreateAPIView {
        get(request: Request, *args: Any, **kwargs: Any) Response
        post(request: Request, *args: Any, **kwargs: Any) Response
    }
    class RetrieveDestroyAPIView {
        get(request: Request, *args: Any, **kwargs: Any) Response
        delete(request: Request, *args: Any, **kwargs: Any) Response
    }
    class RetrieveUpdateAPIView {
        get(request: Request, *args: Any, **kwargs: Any) Response
        put(request: Request, *args: Any, **kwargs: Any) Response
        patch(request: Request, *args: Any, **kwargs: Any) Response
    }
    class RetrieveUpdateDestroyAPIView {
        get(request: Request, *args: Any, **kwargs: Any) Response
        put(request: Request, *args: Any, **kwargs: Any) Response
        patch(request: Request, *args: Any, **kwargs: Any) Response
        delete(request: Request, *args: Any, **kwargs: Any) Response
    }
    GenericAPIView <|-- CreateAPIView
    GenericAPIView <|-- UpdateAPIView
    GenericAPIView <|-- ListAPIView
    GenericAPIView <|-- RetrieveAPIView
    GenericAPIView <|-- ListCreateAPIView
    GenericAPIView <|-- RetrieveDestroyAPIView
    GenericAPIView <|-- RetrieveUpdateAPIView
    GenericAPIView <|-- RetrieveUpdateDestroyAPIView

    class UpdateModelMixin {
        update(request: Request, *args: Any, **kwargs: Any) Response
    }
    class CreateModelMixin {
        create(request: Request, *args: Any, **kwargs: Any) Response
    }
    class ListModelMixin {
        list(request: Request, *args: Any, **kwargs: Any) Response
    }
    class RetrieveModelMixin {
        retrieve(request: Request, *args: Any, **kwargs: Any) Response
    }
Loading

Class diagram for updated viewsets with type annotations

classDiagram
    class GenericViewSet {
    }
    class ModelViewSet {
    }
    class ReadOnlyModelViewSet {
    }
    GenericAPIView <|-- GenericViewSet
    GenericViewSet <|-- ModelViewSet
    GenericViewSet <|-- ReadOnlyModelViewSet
    ModelViewSet <|-- ReadOnlyModelViewSet
    note for GenericViewSet "Now inherits GenericAPIView[_MT_co] and viewsets.GenericViewSet[_MT_co]"
    note for ModelViewSet "Now inherits GenericViewSet[_MT_co]"
    note for ReadOnlyModelViewSet "Now inherits GenericViewSet[_MT_co]"
Loading

File-Level Changes

Change Details Files
Added static typing to API views, mixins, and viewsets
  • Introduced a covariant type variable for model serializers and parameterized GenericAPIView and GenericViewSet
  • Annotated all view and mixin methods with Request, Response, Any, and BaseSerializer[_MT_co] return types
  • Added type: ignore comments to suppress attribute and misc type errors where needed
drf_rw_serializers/generics.py
drf_rw_serializers/mixins.py
drf_rw_serializers/viewsets.py
Upgraded Python support and dependencies
  • Bumped minimum Python version from 3.8 to 3.9 and updated setup.py classifiers
  • Added type stub packages and pre-commit to Poetry dependencies
  • Extended tox.ini and GitHub Actions matrices to cover Python 3.13 and newer DRF/Django combos
pyproject.toml
tox.ini
.github/workflows/tests.yml
setup.py
Integrated strict MyPy configuration
  • Added a [tool.mypy] section with strict flags and overrides for drf_rw_serializers
  • Enabled MyPy checks in .prospector.yaml under mypy section
pyproject.toml
.prospector.yaml
Adjusted tests for changed get_serializer_class behavior
  • Replaced assertIsNone with assertRaises(AssertionError) when no serializer is returned
tests/test_generics.py

Assessment against linked issues

Issue Objective Addressed Explanation
#58 Add type hints (PEP 484 type annotations) to the codebase to enable static type checking and compatibility with DRF's type hint support.
#58 Update dependencies and configuration to support type checking (e.g., add mypy, stubs, and stricter type checking settings).

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • There are many nearly identical HTTP method implementations across the viewset classes—consider abstracting that pattern (e.g. via a base mixin or factory) to reduce duplication.
  • The large number of # type: ignore annotations suggests mypy isn’t recognizing your custom get_read_serializer/get_write_serializer methods—introducing a protocol or adjusting your generics might help remove those ignores.
  • This PR bundles typing additions, version bumps, and a behavioral change in get_serializer_class; it may be clearer to split these concerns into separate PRs to isolate the breaking change.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are many nearly identical HTTP method implementations across the viewset classes—consider abstracting that pattern (e.g. via a base mixin or factory) to reduce duplication.
- The large number of `# type: ignore` annotations suggests mypy isn’t recognizing your custom `get_read_serializer`/`get_write_serializer` methods—introducing a protocol or adjusting your generics might help remove those ignores.
- This PR bundles typing additions, version bumps, and a behavioral change in `get_serializer_class`; it may be clearer to split these concerns into separate PRs to isolate the breaking change.

## Individual Comments

### Comment 1
<location> `drf_rw_serializers/generics.py:22-23` </location>
<code_context>
+
+
+class GenericAPIView(generics.GenericAPIView[_MT_co]):
+    read_serializer_class: type[BaseSerializer[_MT_co]] | None = None
+    write_serializer_class: type[BaseSerializer[_MT_co]] | None = None

-class GenericAPIView(generics.GenericAPIView):
</code_context>

<issue_to_address>
**issue:** Type annotations using 'type[...] | None' may not be compatible with Python <3.10.

To maintain compatibility with Python 3.9, use 'Optional[type[BaseSerializer[_MT_co]]]' instead.
</issue_to_address>

### Comment 2
<location> `example_app/urls.py:22` </location>
<code_context>
 )

-urlpatterns = [
+urlpatterns: list[URLResolver | URLPattern] = [
     path(
         "orders-list-without-read-serializer/",
</code_context>

<issue_to_address>
**issue:** Type annotation using 'list[URLResolver | URLPattern]' may not be compatible with Python <3.10.

For Python 3.9 support, use 'List[Union[URLResolver, URLPattern]]' instead.
</issue_to_address>

### Comment 3
<location> `drf_rw_serializers/mixins.py:12` </location>
<code_context>
         partial = kwargs.pop("partial", False)
-        instance = self.get_object()
-        write_serializer = self.get_write_serializer(instance, data=request.data, partial=partial)
+        instance = self.get_object()  # type: ignore[attr-defined]
+        write_serializer = self.get_write_serializer(  # type: ignore[attr-defined]
+            instance,
</code_context>

<issue_to_address>
**issue (complexity):** Consider replacing all inline type ignore comments with a single module-level or class-level ignore directive to reduce clutter.

Here’s a quick way to get rid of all those inline `# type: ignore[...]` comments and still keep your annotations:

1. At the top of the module (or right on each mixin class), add a single `type: ignore` for the “attr-defined” and “misc” errors you’re silencing:

```python
# -*- coding: utf-8 -*-
# mypy: ignore-errors
# type: ignore[attr-defined, misc]

from typing import Any
from rest_framework import mixins, status
from rest_framework.request import Request
from rest_framework.response import Response
```

2. Remove all the per-call ignores. For example, your `UpdateModelMixin` becomes:

```python
class UpdateModelMixin(mixins.UpdateModelMixin):
    def update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
        partial = kwargs.pop("partial", False)
        instance = self.get_object()
        write_serializer = self.get_write_serializer(
            instance,
            data=request.data,
            partial=partial,
        )
        write_serializer.is_valid(raise_exception=True)
        self.perform_update(write_serializer)

        # pylint: disable=protected-access
        if getattr(instance, "_prefetched_objects_cache", None) is not None:
            instance._prefetched_objects_cache = {}
        # pylint: enable=protected-access

        read_serializer = self.get_read_serializer(instance)
        return Response(read_serializer.data)
```

Repeat for your other mixins (Create/List/Retrieve).  

By silencing the errors once at the top (or on each class), you preserve all your type-hints and remove the noise of dozens of inline ignores.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread drf_rw_serializers/generics.py Outdated
Comment thread example_app/urls.py Outdated
Comment thread drf_rw_serializers/mixins.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add support for type hints

1 participant