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

[DI-2708] Ruff ALL rules #91

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ jobs:
cd build/awesome
cp ./api/.env.example ./api/.env
make api-test
- name: Ruff, Safety, migrations checks for the default project code
run: |
cd build/awesome
make api-check
2 changes: 1 addition & 1 deletion {{ cookiecutter.project_slug }}/api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ collectstatic:
check:
ruff format . --check \
&& ruff check . \
&& safety check \
&& safety check --ignore=70612 \
&& make check-migrations \

check-fix:
Expand Down
10 changes: 9 additions & 1 deletion {{ cookiecutter.project_slug }}/api/conftest.py
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
from {{ cookiecutter.project_slug }}.fixtures import * # noqa: W0401, W0611
from {{ cookiecutter.project_slug }}.fixtures.api_client import api_client, unauthorized_api_client
from {{ cookiecutter.project_slug }}.fixtures.user_account import user_account


__all__ = [
"api_client",
"unauthorized_api_client",
"user_account",
]
7 changes: 4 additions & 3 deletions {{ cookiecutter.project_slug }}/api/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@
import sys


def main():
def main() -> None:
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ cookiecutter.project_slug }}.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
message = (
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
)
raise ImportError(message) from exc
execute_from_command_line(sys.argv)


Expand Down
39 changes: 19 additions & 20 deletions {{ cookiecutter.project_slug }}/api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,8 @@ DJANGO_SETTINGS_MODULE = "{{ cookiecutter.project_slug }}.settings"
python_files = ["tests.py", "test_*.py", "*_test.py"]

[tool.ruff]
# Enable pycodestyle (`E`), Pyflakes (`F`), Isort (`I`), Bandit (`S`) and flake8 Django (`DJ`) codes.
lint.select = ["E", "F", "I", "S", "DJ"]
lint.ignore = []

# Allow autofix for all enabled rules (when `--fix`) is provided.
lint.fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
lint.unfixable = []
line-length = 120
target-version = "py{{ cookiecutter.python_version.lower().replace('.', '') }}"

# Exclude a variety of commonly ignored directories.
exclude = [
Expand All @@ -69,28 +64,32 @@ exclude = [
"venv",
]

line-length = 120

# Allow unused variables when underscore-prefixed.
lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
target-version = "py{{ cookiecutter.python_version.lower()|replace('.', '') }}"

[tool.ruff.lint.mccabe]
# Unlike Flake8, default to a complexity level of 10.
max-complexity = 10
[tool.ruff.lint]
select = ["ALL"]
ignore = [
# Docstrings
"D",
# Assert statement
"S101",
# Unnecessary-assign before return
"RET504",
# Random for non cryptographically secure
"S311",
# Formatter related rules...
"COM812", "ISC001",
]

[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401", "F403"]
"conftest*" = ["F403"]
"test_*.py" = ["S"]
"tests.py" = ["S"]
"*_test.py" = ["S"]
"*/migrations/*" = ["RUF012"]

[tool.ruff.lint.isort]
lines-between-types = 1
lines-after-imports = 2
known-first-party=['{{ cookiecutter.project_slug }}']
section-order= ['future', 'standard-library', 'third-party', 'django', 'first-party', 'local-folder']
known-first-party=["{{ cookiecutter.project_slug }}"]
section-order= ["future", "standard-library", "third-party", "django", "first-party", "local-folder"]

[tool.ruff.lint.isort.sections]
"django" = ["django"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from rest_framework.authentication import SessionAuthentication
from rest_framework.request import Request


class CustomSessionAuthentication(SessionAuthentication):
def enforce_csrf(self, request):
def enforce_csrf(self, request: Request) -> None:
"""
Exempt CSRF for session based authentication "application/json".
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from rest_framework.permissions import BasePermission
from rest_framework.request import Request

from django.views import View


class IsNotAuthenticated(BasePermission):
def has_permission(self, request, view):
def has_permission(self, request: Request, view: View) -> bool: # noqa: ARG002
return not request.user.is_authenticated
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@
from django.contrib.auth import authenticate as django_authenticate
from django.utils.translation import gettext

from {{ cookiecutter.project_slug }}.apps.accounts.models import UserAccount


class LoginSerializer(serializers.Serializer):
email = serializers.EmailField(write_only=True, max_length=254)
password = serializers.CharField(max_length=128, style={"input_type": "password"}, write_only=True)

@staticmethod
def _authenticate(email, password):
def _authenticate(email: str, password: str) -> UserAccount | None:
return django_authenticate(email=email, password=password) # pragma: no cover

def validate(self, attrs):
def validate(self, attrs: dict) -> dict:
user = self._authenticate(attrs.get("email"), attrs.get("password"))
if user:
return {"user": user}
raise ValidationError(gettext("Incorrect email or password."))

def create(self, validated_data):
assert False, "Do not use update directly" # noqa: S101
def create(self, validated_data: dict) -> None: # noqa: ARG002
message = "Do not use create directly"
raise AssertionError(message)

def update(self, instance, validated_data):
assert False, "Do not use update directly" # noqa: S101
def update(self, instance: dict, validated_data: dict) -> None: # noqa: ARG002
message = "Do not use update directly"
raise AssertionError(message)
Original file line number Diff line number Diff line change
Expand Up @@ -13,73 +13,77 @@ class ChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(max_length=128, write_only=True, style={"input_type": "password"})
new_password = serializers.CharField(max_length=128, write_only=True, style={"input_type": "password"})

def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002 ANN003
super().__init__(*args, **kwargs)
self.request = self.context.get("request")
self.user = getattr(self.request, "user", None)
self.password_service = PasswordService()

def validate_old_password(self, old_password):
def validate_old_password(self, old_password: str) -> str:
try:
self.password_service.check_password(self.user, old_password)
except WrongPasswordError as e:
raise serializers.ValidationError(e.message) from e
return old_password

def validate_new_password(self, new_password):
def validate_new_password(self, new_password: str) -> str:
try:
self.password_service.validate_password(new_password, user=self.user)
except InvalidPasswordError as e:
raise serializers.ValidationError(e.messages) from e
return new_password

def create(self, validated_data):
assert False, "Do not use update directly" # noqa: S101
def create(self, validated_data: dict) -> None: # noqa: ARG002
message = "Do not use update directly"
raise AssertionError(message)

def update(self, instance, validated_data):
assert False, "Do not use update directly" # noqa: S101
def update(self, instance: dict, validated_data: dict) -> None: # noqa: ARG002
message = "Do not use update directly"
raise AssertionError(message)

def save(self, **kwargs):
def save(self, **_kwargs) -> None: # noqa: ANN003
self.password_service.change_password(self.user, self.validated_data["new_password"])


class ConfirmResetPasswordSerializer(serializers.Serializer):
password = serializers.CharField(max_length=128, write_only=True, style={"input_type": "password"})
signature = serializers.CharField(max_length=71, write_only=True)

def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002 ANN003
super().__init__(*args, **kwargs)
self.password_service = PasswordService()

def validate_password(self, password):
def validate_password(self, password: str) -> str:
try:
self.password_service.validate_password(password)
except InvalidPasswordError as e:
raise ValidationError(e) from e
return password

def save(self, **kwargs):
def save(self, **_kwargs) -> None: # noqa: ANN003
new_password = self.validated_data["password"]
signature = self.validated_data["signature"]
try:
self.password_service.reset_password(signature, new_password)
except InvalidResetPasswordSignatureError as e:
raise ValidationError({"signature": e.message}) from e

def create(self, validated_data):
assert False, "Do not use update directly" # noqa: S101
def create(self, validated_data: dict) -> None: # noqa: ARG002
message = "Do not use create directly"
raise AssertionError(message)

def update(self, instance, validated_data):
assert False, "Do not use update directly" # noqa: S101
def update(self, instance: dict, validated_data: dict) -> None: # noqa: ARG002
message = "Do not use update directly"
raise AssertionError(message)


class ResetPasswordSerializer(serializers.Serializer):
email = serializers.EmailField(max_length=128, write_only=True)

def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002 ANN003
super().__init__(*args, **kwargs)
self.password_service = PasswordService()

def save(self, **kwargs):
def save(self, **_kwargs) -> None: # noqa: ANN003
email = self.validated_data["email"]
self.password_service.send_reset_password_link(email)
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,23 @@ class Meta:
model = UserAccount
fields = ("email", "first_name", "last_name", "password")

def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002 ANN003
super().__init__(*args, **kwargs)
self.password_service = PasswordService()

def validate_email(self, email):
def validate_email(self, email: str) -> str:
if UserAccount.objects.filter(email=email).exists():
raise ValidationError(gettext("Could not create account with this email."))
return super().validate(email)

def validate_password(self, new_password):
def validate_password(self, new_password: str) -> str:
try:
self.password_service.validate_password(new_password)
except InvalidPasswordError as e:
raise serializers.ValidationError(e.messages) from e
return new_password

def save(self, **kwargs):
def save(self, **kwargs) -> UserAccount: # noqa: ANN003
self.instance = super().save(**kwargs)
raw_password = self.validated_data.get("password")
self.instance.set_password(raw_password)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response

from {{ cookiecutter.project_slug }}.apps.accounts.api.permissions import IsNotAuthenticated
from {{ cookiecutter.project_slug }}.apps.accounts.api.v1.serializers.login import LoginSerializer
from {{ cookiecutter.project_slug }}.apps.accounts.services.login import LoginService


class LoginView(GenericAPIView):
permission_classes = [IsNotAuthenticated]
permission_classes = (IsNotAuthenticated,)
serializer_class = LoginSerializer

@extend_schema(summary="Login", tags=["Accounts"], responses={status.HTTP_204_NO_CONTENT: OpenApiResponse()})
def post(self, request):
def post(self, request: Request) -> Response:
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data.get("user")
Expand All @@ -22,9 +24,9 @@ def post(self, request):


class LogoutView(GenericAPIView):
permission_classes = [IsAuthenticated]
permission_classes = (IsAuthenticated,)

@extend_schema(summary="Log out", tags=["Accounts"], responses={status.HTTP_204_NO_CONTENT: OpenApiResponse()})
def post(self, request):
def post(self, request: Request) -> Response:
response = LoginService.logout(request)
return response
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from rest_framework import status
from rest_framework.generics import CreateAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response

from {{ cookiecutter.project_slug }}.apps.accounts.api.v1.serializers.password import (
Expand All @@ -18,7 +19,7 @@ class ChangePasswordAPIView(CreateAPIView):
@extend_schema(
summary="Change password", tags=["Accounts"], responses={status.HTTP_204_NO_CONTENT: OpenApiResponse()}
)
def post(self, request, *args, **kwargs): # pragma: no cover
def post(self, request: Request, *args, **kwargs) -> Response: # noqa: ANN002 ANN003
super().post(request, *args, **kwargs)
return Response(status=status.HTTP_204_NO_CONTENT)

Expand All @@ -29,7 +30,7 @@ class ResetPasswordAPIView(CreateAPIView):
@extend_schema(
summary="Reset password", tags=["Accounts"], responses={status.HTTP_204_NO_CONTENT: OpenApiResponse()}
)
def post(self, request, *args, **kwargs): # pragma: no cover
def post(self, request: Request, *args, **kwargs) -> Response: # noqa: ANN002 ANN003
super().post(request, *args, **kwargs)
return Response(status=status.HTTP_204_NO_CONTENT)

Expand All @@ -40,6 +41,6 @@ class ConfirmResetPasswordAPIView(CreateAPIView):
@extend_schema(
summary="Confirm reset password", tags=["Accounts"], responses={status.HTTP_204_NO_CONTENT: OpenApiResponse()}
)
def post(self, request, *args, **kwargs): # pragma: no cover
def post(self, request: Request, *args, **kwargs) -> Response: # noqa: ANN002 ANN003
super().post(request, *args, **kwargs)
return Response(status=status.HTTP_204_NO_CONTENT)
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework import status
from rest_framework.generics import GenericAPIView
from rest_framework.request import Request
from rest_framework.response import Response

from {{ cookiecutter.project_slug }}.apps.accounts.api.v1.serializers.registration import RegistrationSerializer
from {{ cookiecutter.project_slug }}.apps.accounts.services.login import LoginService
Expand All @@ -10,7 +12,7 @@ class RegistrationAPIView(GenericAPIView):
serializer_class = RegistrationSerializer

@extend_schema(summary="Registration", tags=["Accounts"], responses={status.HTTP_204_NO_CONTENT: OpenApiResponse()})
def post(self, request):
def post(self, request: Request) -> Response:
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rest_framework.permissions import IsAuthenticated

from {{ cookiecutter.project_slug }}.apps.accounts.api.v1.serializers.user_profile import UserProfileSerializer
from {{ cookiecutter.project_slug }}.apps.accounts.models import UserAccount


@extend_schema_view(
Expand All @@ -21,7 +22,7 @@
)
class UserProfileAPIView(RetrieveUpdateAPIView):
serializer_class = UserProfileSerializer
permission_classes = [IsAuthenticated]
permission_classes = (IsAuthenticated,)

def get_object(self):
def get_object(self) -> UserAccount:
return self.request.user
Loading