Skip to content

Commit

Permalink
Add type hints (#21)
Browse files Browse the repository at this point in the history
* Add type hints

* Redo mypy testing via tox

* Actually use tox in travis build

* Fix tox env usage

* Add Python 3.9 to tox list

* django2.2 -> django2 in tox

* Upgrade pytest-django

* Django 3.x doesn't support Python 3.5

* Deal with FieldDoesNotExist issues

* Explicitly set Python versions

* DRF 3.8/3.9 don't work well with Django 3

* Correct flake8/docs names

* Fix the multiple django specs

* Set deploy job to a TOXENV that still exists

* Fix mypy deps in tox

* Fix lint issues

* Ignore mypy imports for flake8
  • Loading branch information
palfrey authored Jul 11, 2021
1 parent 2f6fb32 commit bac4b70
Show file tree
Hide file tree
Showing 8 changed files with 52 additions and 27 deletions.
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ jobs:
env: TOXENV=py37-flake8
- python: 3.7
env: TOXENV=py37-docs
- python: 3.7
env: TOXENV=py37-mypy
- python: 3.5
env: TOXENV=py35-django2-drf3.8
- python: 3.5
Expand Down
36 changes: 26 additions & 10 deletions dry_rest_permissions/generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,26 @@
they can be consumed by a front end application.
"""
from functools import wraps
from typing import Type, cast
from django.db.models.query import QuerySet

from rest_framework import filters
from rest_framework import permissions
from rest_framework import fields
from rest_framework import request
from rest_framework import views
from rest_framework import generics


from django.contrib.contenttypes.models import ContentType
from rest_framework.serializers import ModelSerializer

# gets redefined as true when type checking
MYPY = False

if MYPY:
from django.db import models as django_models # noqa: F401
from typing import List # noqa: F401


class DRYPermissionFiltersBase(filters.BaseFilterBackend):
Expand All @@ -34,23 +48,23 @@ class DRYPermissionFiltersBase(filters.BaseFilterBackend):
"""
action_routing = False

def filter_queryset(self, request, queryset, view):
def filter_queryset(self, request: request.Request, queryset: QuerySet, view: views.APIView):
"""
This method overrides the standard filter_queryset method.
This method will check to see if the view calling this is from
a list type action. This function will also route the filter
by action type if action_routing is set to True.
"""
# Check if this is a list type request
if view.lookup_field not in view.kwargs:
if isinstance(view, generics.GenericAPIView) and view.lookup_field not in view.kwargs:
if not self.action_routing:
return self.filter_list_queryset(request, queryset, view)
else:
method_name = "filter_{action}_queryset".format(action=view.action)
method_name = "filter_{action}_queryset".format(action=view.action) # type: ignore[attr-defined]
return getattr(self, method_name)(request, queryset, view)
return queryset

def filter_list_queryset(self, request, queryset, view):
def filter_list_queryset(self, request: request.Request, queryset: QuerySet, view: views.APIView) -> QuerySet:
"""
Override this function to add filters.
This should return a queryset so start with queryset.filter({your filters})
Expand Down Expand Up @@ -96,14 +110,16 @@ class DRYPermissions(permissions.BasePermission):
object_permissions = True
partial_update_is_update = True

def has_permission(self, request, view):
def has_permission(self, request: request.Request, view: views.APIView):
"""
Overrides the standard function and figures out methods to call for global permissions.
"""
if not self.global_permissions:
return True

serializer_class = view.get_serializer_class()
assert isinstance(view, generics.GenericAPIView), "View needs to descend from GenericAPIView"

serializer_class = cast(Type[ModelSerializer], view.get_serializer_class())

assert serializer_class.Meta.model is not None, (
"global_permissions set to true without a model "
Expand All @@ -114,7 +130,7 @@ def has_permission(self, request, view):

action_method_name = None
if hasattr(view, 'action'):
action = self._get_action(view.action)
action = self._get_action(view.action) # type: ignore[attr-defined]
action_method_name = "has_{action}_permission".format(action=action)
# If the specific action permission exists then use it, otherwise use general.
if hasattr(model_class, action_method_name):
Expand All @@ -123,11 +139,11 @@ def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
assert hasattr(model_class, 'has_read_permission'), \
self._get_error_message(model_class, 'has_read_permission', action_method_name)
return model_class.has_read_permission(request)
return model_class.has_read_permission(request) # type: ignore[attr-defined]
else:
assert hasattr(model_class, 'has_write_permission'), \
self._get_error_message(model_class, 'has_write_permission', action_method_name)
return model_class.has_write_permission(request)
return model_class.has_write_permission(request) # type: ignore[attr-defined]

def has_object_permission(self, request, view, obj):
"""
Expand Down Expand Up @@ -299,7 +315,7 @@ class DRYGlobalPermissionsField(fields.Field):
'write',
'read'
]
models = []
models = [] # type: List[django_models.Model]

def __init__(self, actions=None, additional_actions=None, **kwargs):
"""See class description for parameters and usage"""
Expand Down
Empty file added dry_rest_permissions/py.typed
Empty file.
6 changes: 6 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ wheel==0.24.0

# MkDocs for documentation previews/deploys
mkdocs==1.2.1

# mypy
mypy==0.910
django-stubs==1.8.0
djangorestframework-stubs==1.4.0
types-pyyaml==5.4.3
9 changes: 9 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
[wheel]
universal = 1

[mypy]
show_error_codes = True
plugins =
mypy_django_plugin.main,
mypy_drf_plugin.main

[mypy.plugins.django-stubs]
django_settings_module = "tests.settings"
18 changes: 1 addition & 17 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,6 @@ def get_packages(package):
if os.path.exists(os.path.join(dirpath, '__init__.py'))]


def get_package_data(package):
"""
Return all files under the root package, that are not in a
package themselves.
"""
walk = [(dirpath.replace(package + os.sep, '', 1), filenames)
for dirpath, dirnames, filenames in os.walk(package)
if not os.path.exists(os.path.join(dirpath, '__init__.py'))]

filepaths = []
for base, filenames in walk:
filepaths.extend([os.path.join(base, filename)
for filename in filenames])
return {package: filepaths}


version = get_version(package)


Expand All @@ -81,7 +65,7 @@ def get_package_data(package):
author=author,
author_email=author_email,
packages=get_packages(package),
package_data=get_package_data(package),
package_data={"dry_rest_permissions": ["py.typed"]},
install_requires=[],
classifiers=[
'Development Status :: 5 - Production/Stable',
Expand Down
Empty file added tests/settings.py
Empty file.
8 changes: 8 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@ deps =
commands = mkdocs build
deps =
mkdocs>=0.11.1

[testenv:py37-mypy]
commands = mypy dry_rest_permissions
deps =
mypy==0.910
django-stubs==1.8.0
djangorestframework-stubs==1.4.0
types-pyyaml==5.4.3

0 comments on commit bac4b70

Please sign in to comment.