Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints #49

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/django_grainy/backends.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re
from typing import Any, Optional

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User

from .helpers import django_op_to_flag
from .models import namespace
Expand All @@ -13,14 +15,16 @@ class GrainyBackend(ModelBackend):
Authenticate actions using grainy permissions
"""

def has_module_perms(self, user, obj=None):
def has_module_perms(self, user: User, obj: str = None) -> bool:

# superusers have access to everything
if user.is_superuser:
return True

return Permissions(user).check(obj, django_op_to_flag("view"))

def has_perm(self, user, perm, obj=None):
def has_perm(self, user: User, perm: str, obj: Optional[Any] = None) -> bool:

# superusers have access to everything
if user.is_superuser:
return True
Expand Down
82 changes: 62 additions & 20 deletions src/django_grainy/decorators.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import inspect
import json
from typing import Any, Callable, Optional, Type, Union

from django.core.handlers.wsgi import WSGIRequest
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import Model
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.http.response import HttpResponse, JsonResponse
from django.views import View
from grainy.core import Namespace
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer

from .exceptions import DecoratorRequiresNamespace
from .handlers import GrainyHandler, GrainyModelHandler
Expand All @@ -27,15 +35,20 @@ class grainy_decorator:
# if true, this decorator cannot have a None namespace
require_namespace = False

def __init__(self, namespace=None, namespace_instance=None, **kwargs):
def __init__(
self,
namespace: Optional[str] = None,
namespace_instance: Optional[Namespace] = None,
**kwargs,
):
self.namespace = namespace
self.namespace_instance = namespace_instance
self.extra = kwargs
self.permissions_cls = kwargs.get("permissions_cls", Permissions)
if self.require_namespace and not namespace:
raise DecoratorRequiresNamespace(self)

def make_grainy_handler(self, target):
def make_grainy_handler(self, target: Any) -> GrainyHandler:
class Grainy(self.handler_class):
pass

Expand Down Expand Up @@ -68,11 +81,13 @@ class grainy_model(grainy_decorator):

handler_class = GrainyModelHandler

def __init__(self, namespace=None, parent=None, **kwargs):
def __init__(
self, namespace: Optional[str] = None, parent: Optional[str] = None, **kwargs
) -> grainy_decorator:
self.parent = parent
return super().__init__(namespace=namespace, **kwargs)

def __call__(self, model):
def __call__(self, model: Model) -> Model:
model.Grainy = self.make_grainy_handler(model)
if self.parent:
model.Grainy.parent_field = self.parent
Expand All @@ -88,7 +103,7 @@ def __call__(self, model):

return model

def parent_namespacing(self, model):
def parent_namespacing(self, model: Model):
namespace = [model.Grainy.namespace_instance_template]
if not namespace:
namespace = [model.Grainy.namespace(), "{instance.pk}"]
Expand Down Expand Up @@ -137,7 +152,8 @@ class grainy_view_response(grainy_decorator):

view = None

def __call__(self, view_function):
def __call__(self, view_function: Callable) -> Callable:

get_object = self.get_object
apply_perms = self.apply_perms
extra = self.extra
Expand All @@ -146,7 +162,9 @@ def __call__(self, view_function):

grainy_handler = self.make_grainy_handler(view_function)

def response_handler(*args, **kwargs):
def response_handler(
*args: Any, **kwargs: Any
) -> Union[HttpResponse, JsonResponse, Response]:
if isinstance(args[0], HttpRequest):
self = None
request = args[0]
Expand Down Expand Up @@ -195,7 +213,7 @@ def response_handler(*args, **kwargs):
response_handler.__name__ = view_function.__name__
return response_handler

def get_object(self, view):
def get_object(self, view: View) -> Optional[Any]:
"""
Attempts to call and return `get_object` on the decorated view.

Expand All @@ -207,13 +225,19 @@ def get_object(self, view):
return view.get_object()
return None

def apply_perms(self, request, response, view_function, view):
def apply_perms(
self,
request: WSGIRequest,
response: HttpResponse,
view_function: Callable,
view: View,
) -> HttpResponse:
"""
Apply permissions to the generated response
"""
return response

def augment_request(self, request):
def augment_request(self, request: WSGIRequest) -> WSGIRequest:
"""
Augment the request instance
"""
Expand All @@ -234,7 +258,13 @@ class grainy_json_view_response(grainy_view_response):
- json_dumps_params <dict>: passed to DjangoJSONEncoder
"""

def _apply_perms(self, request, data, view_function, view):
def _apply_perms(
self,
request: Union[WSGIRequest, Request],
data: Any,
view_function: Callable,
view: View,
) -> Union[dict, list]:
perms = self.permissions_cls(request.user)
try:
self.get_object(view)
Expand Down Expand Up @@ -264,7 +294,13 @@ def _apply_perms(self, request, data, view_function, view):
else:
return {}

def apply_perms(self, request, response, view_function, view):
def apply_perms(
self,
request: WSGIRequest,
response: JsonResponse,
view_function: Callable,
view: View,
) -> JsonResponse:
response.content = JsonResponse(
self._apply_perms(
request,
Expand Down Expand Up @@ -298,17 +334,19 @@ class grainy_rest_viewset_response(grainy_json_view_response):

require_namespace = True

def get_object(self, view):
def get_object(self, view: View) -> Optional[Any]:
try:
return super().get_object(view)
except AssertionError:
return None

def apply_perms(self, request, response, view_function, view):
def apply_perms(
self, request: Request, response: Response, view_function: Callable, view: View
) -> Response:
response.data = self._apply_perms(request, response.data, view_function, view)
return response

def augment_request(self, request):
def augment_request(self, request: Request) -> Request:
"""
Augments the request by adding the following methods

Expand All @@ -325,7 +363,8 @@ def augment_request(self, request):
)
perms = decorator.permissions_cls(request.user)

def grainy_data(request, defaults):
def grainy_data(request: Request, defaults: dict):

"""
Returns a cleaned up dict for request.data

Expand Down Expand Up @@ -353,7 +392,9 @@ def grainy_data(request, defaults):

return data

def grainy_update_serializer(serializer_cls, instance, **kwargs):
def grainy_update_serializer(
serializer_cls: Type[Serializer], instance: Serializer, **kwargs
) -> Request:
"""
returns a django-rest-framework serializer instance with
for saves.
Expand Down Expand Up @@ -389,12 +430,13 @@ class grainy_view(grainy_decorator):
decorator = grainy_view_response
require_namespace = True

def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any):
self.args = args
self.kwargs = kwargs
super().__init__(*args, **kwargs)

def __call__(self, view):
def __call__(self, view: View):

view.Grainy = self.make_grainy_handler(view)

if inspect.isclass(view):
Expand All @@ -413,7 +455,7 @@ def __call__(self, view):
else:
return self.decorate(view)

def decorate(self, view):
def decorate(self, view: View):
return self.decorator(*self.args, **self.kwargs)(view)


Expand Down
7 changes: 5 additions & 2 deletions src/django_grainy/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from typing import Any, Callable


class DecoratorRequiresNamespace(ValueError):
def __init__(self, decorator):
def __init__(self, decorator: Callable):
super().__init__("This decorator requires you to specify a namespace")
self.decorator = decorator


class PermissionDenied(Exception):
def __init__(self, reason):
def __init__(self, reason: Any):
super().__init__(f"Permission denied: {reason}")
8 changes: 5 additions & 3 deletions src/django_grainy/fields.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import List, Union

from django import forms
from django.db import models

from .conf import PERM_CHOICES


class PermissionFormField(forms.IntegerField):
def prepare_value(self, value):
def prepare_value(self, value: Union[List[int], int]) -> List[int]:
# if the form field is passed a bitmask we
# need to convert it to a list, where each
# item represents a choice (flag)
Expand All @@ -19,7 +21,7 @@ def prepare_value(self, value):
value = value or 0
return value

def clean(self, value):
def clean(self, value: List[int]) -> int:
if isinstance(value, list):
_value = 0
for flag in value:
Expand All @@ -29,7 +31,7 @@ def clean(self, value):


class PermissionField(models.IntegerField):
def to_python(self, value):
def to_python(self, value: Union[int, str]) -> int:
# if a string is passed it should be parsed
# for string flags (for example 'c', 'r', 'u' and 'd')
# as specified in the PERM_CHOICES setting and
Expand Down
17 changes: 12 additions & 5 deletions src/django_grainy/handlers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Any, Union

from django.db.models import Model
from grainy.core import Namespace


Expand All @@ -11,7 +14,9 @@ class GrainyHandler:
namespace_instance_template = "{namespace}.{instance}"

@classmethod
def namespace_instance(cls, instance, **kwargs):
def namespace_instance(
cls, instance: Union[object, str, Namespace], **kwargs: Any
) -> str:
"""
Returns the permissioning namespace for the passed instance

Expand Down Expand Up @@ -45,7 +50,9 @@ def namespace_instance(cls, instance, **kwargs):
).lower()

@classmethod
def namespace(cls, instance=None, **kwargs):
def namespace(
cls, instance: Union[object, str, Namespace] = None, **kwargs: Any
) -> str:
"""
Wrapper function to return either the result of namespace_base or
namespace instance depending on whether or not a value was passed in
Expand All @@ -68,13 +75,13 @@ def namespace(cls, instance=None, **kwargs):
return namespace.lower()

@classmethod
def set_namespace_base(cls, value):
def set_namespace_base(cls, value: Namespace):
if not isinstance(value, Namespace):
raise TypeError("`value` needs to be a Namespace instance")
cls.namespace_base = value

@classmethod
def set_parent(cls, parent):
def set_parent(cls, parent: Model):
cls.parent = parent


Expand All @@ -88,7 +95,7 @@ class GrainyModelHandler(GrainyHandler):
namespace_instance_template = "{namespace}.{instance.pk}"

@classmethod
def set_parent(cls, model):
def set_parent(cls, model: Model):
cls.parent = model
cls.model = model
cls.set_namespace_base(
Expand Down
Loading
Loading