From eaf730f457c06c9aba810ad3d61cd2f45b955c22 Mon Sep 17 00:00:00 2001
From: Neda Moeini <neda.moeini@geant.org>
Date: Mon, 7 Oct 2024 12:18:14 +0200
Subject: [PATCH] Add tox, mypy and ruff and all the fixes based on ruff and
 mypy suggestions.

---
 docs/source/conf.py         |  3 ++
 file_validator/admin.py     |  4 +--
 file_validator/apps.py      |  3 ++
 file_validator/forms.py     | 16 ++++++----
 file_validator/models.py    |  4 +--
 file_validator/tests.py     |  4 +--
 file_validator/urls.py      |  9 ++++--
 file_validator/views.py     | 59 +++++++++++++++++++------------------
 pyproject.toml              | 40 +++++++++++++++++++++++++
 sage_validation/settings.py |  2 +-
 sage_validation/urls.py     | 17 +----------
 setup.py                    | 17 ++++++-----
 tox.ini                     | 10 +++++++
 13 files changed, 116 insertions(+), 72 deletions(-)
 create mode 100644 pyproject.toml
 create mode 100644 tox.ini

diff --git a/docs/source/conf.py b/docs/source/conf.py
index 67a5149..5cb62b0 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -31,6 +31,9 @@ release = '0.1'
 # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
 # ones.
 extensions = [
+    'sphinx.ext.autodoc',
+    'sphinx.ext.napoleon',
+    'sphinx_autodoc_typehints',
 ]
 
 # Add any paths that contain templates here, relative to this directory.
diff --git a/file_validator/admin.py b/file_validator/admin.py
index 8c38f3f..eb15018 100644
--- a/file_validator/admin.py
+++ b/file_validator/admin.py
@@ -1,3 +1 @@
-from django.contrib import admin
-
-# Register your models here.
+"""Admin configuration for the file_validator app."""
diff --git a/file_validator/apps.py b/file_validator/apps.py
index 549ab43..171df15 100644
--- a/file_validator/apps.py
+++ b/file_validator/apps.py
@@ -1,6 +1,9 @@
+"""App configuration for file_validator app."""
 from django.apps import AppConfig
 
 
 class FileValidatorConfig(AppConfig):
+    """App configuration for file_validator."""
+
     default_auto_field = "django.db.models.BigAutoField"
     name = "file_validator"
diff --git a/file_validator/forms.py b/file_validator/forms.py
index 275ffb9..835729a 100644
--- a/file_validator/forms.py
+++ b/file_validator/forms.py
@@ -1,14 +1,18 @@
-# file_validator/forms.py
+"""Form for uploading CSV files."""
 from django import forms
 
 
 class CSVUploadForm(forms.Form):
-    file = forms.FileField(label='Select a CSV file')
+    """Form for uploading CSV files."""
 
-    def clean_file(self):
-        file = self.cleaned_data['file']
+    file = forms.FileField(label="Select a CSV file")
+
+    def clean_file(self) -> str:
+        """Check if the file is a CSV file."""
+        file = self.cleaned_data["file"]
 
         # Check if the file is a CSV
-        if not file.name.endswith('.csv'):
-            raise forms.ValidationError("Only CSV files are allowed")
+        if not file.name.endswith(".csv"):
+            err_msg = "Only CSV files are allowed"
+            raise forms.ValidationError(err_msg)
         return file
diff --git a/file_validator/models.py b/file_validator/models.py
index 71a8362..5fd99e9 100644
--- a/file_validator/models.py
+++ b/file_validator/models.py
@@ -1,3 +1 @@
-from django.db import models
-
-# Create your models here.
+"""Models for the file_validator app."""
diff --git a/file_validator/tests.py b/file_validator/tests.py
index 7ce503c..676ebd6 100644
--- a/file_validator/tests.py
+++ b/file_validator/tests.py
@@ -1,3 +1 @@
-from django.test import TestCase
-
-# Create your tests here.
+"""All the tests for the file_validator app."""
diff --git a/file_validator/urls.py b/file_validator/urls.py
index 45b8476..af5c733 100644
--- a/file_validator/urls.py
+++ b/file_validator/urls.py
@@ -1,5 +1,8 @@
+"""Urls for the file_validator app."""
 from django.urls import path
-from .views import CSVUploadView
+
+from file_validator.views import CSVUploadView
+
 urlpatterns = [
-    path('upload/', CSVUploadView.as_view(), name='upload-file'),
-]
\ No newline at end of file
+    path("upload/", CSVUploadView.as_view(), name="upload-file"),
+]
diff --git a/file_validator/views.py b/file_validator/views.py
index 23ccaee..5850fbf 100644
--- a/file_validator/views.py
+++ b/file_validator/views.py
@@ -1,56 +1,57 @@
+"""Contains the views for the file_validator app."""
 import csv
-from django.views.generic.edit import FormView
-from django.urls import reverse_lazy
+
 from django.http import JsonResponse
-from .forms import CSVUploadForm
+from django.urls import reverse_lazy
+from django.views.generic.edit import FormView
+
+from file_validator.forms import CSVUploadForm
 
 
 class CSVUploadView(FormView):
-    template_name = 'upload.html'
+    """View for uploading a CSV file."""
+
+    template_name = "upload.html"
     form_class = CSVUploadForm
-    success_url = reverse_lazy('upload-file')
+    success_url = reverse_lazy("upload-file")
 
-    def get_context_data(self, **kwargs):
-        """
-        This method is called when rendering the form (GET request).
-        """
+    def get_context_data(self, **kwargs: dict) -> dict:
+        """Render the form with no error message on GET request."""
         context = super().get_context_data(**kwargs)
-        context['error'] = None  # No error message on GET request
-        context['message'] = None  # No success message on GET request
+        context["error"] = None  # No error message on GET request
+        context["message"] = None  # No success message on GET request
         return context
 
-    def form_valid(self, form):
-        """
-        This method is called when the form is valid (POST request).
-        It handles the CSV validation and passes appropriate error or success messages to the template.
-        """
-        file_obj = form.cleaned_data['file']
+    def form_valid(self, form: CSVUploadForm) -> JsonResponse:
+        """Handle the CSV validation and passes appropriate error or success messages to the template."""
+        file_obj = form.cleaned_data["file"]
 
         try:
             # Read and decode the CSV file
-            csv_file = file_obj.read().decode('utf-8').splitlines()
+            csv_file = file_obj.read().decode("utf-8").splitlines()
             reader = csv.DictReader(csv_file)
 
             # Example validation: Check for required columns
             # TODO: Add the validation logic here. This is just a sample.
-            required_columns = ['name']
-            missing_columns = [col for col in required_columns if col not in reader.fieldnames]
+            required_columns = ["name"]
+            fieldnames = reader.fieldnames if reader.fieldnames is not None else []
+            missing_columns = [col for col in required_columns if col not in fieldnames]
 
             if missing_columns:
                 # If there are missing columns, return error message as JSON
                 return JsonResponse(
-                    {'status': 'error', 'errors': [f"Missing columns: {', '.join(missing_columns)}"]}, status=400
+                    {"status": "error", "errors": [f"Missing columns: {", ".join(missing_columns)}"]}, status=400
                 )
 
-            return JsonResponse({'status': 'success', 'message': "CSV file is valid"})
+            return JsonResponse({"status": "success", "message": "CSV file is valid"})
 
-        except Exception as e:
-            # If there's an error (e.g., invalid file content), return the error message as JSON
-            return JsonResponse({'status': 'error', 'errors': [str(e)]}, status=400)
+        except (UnicodeDecodeError, csv.Error) as e:
+            # If there"s an error (e.g., invalid file content), return the error message as JSON
+            return JsonResponse({"status": "error", "errors": [str(e)]}, status=400)
+
+    def form_invalid(self, form: CSVUploadForm) -> JsonResponse:
+        """Handle the form when it is invalid (e.g., wrong file type).
 
-    def form_invalid(self, form):
-        """
-        This method is called when the form is invalid (e.g., wrong file type).
         It renders the form again with errors.
         """
-        return JsonResponse({'status': 'error', 'errors': [form.errors]}, status=400)
+        return JsonResponse({"status": "error", "errors": [form.errors]}, status=400)
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..491a227
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,40 @@
+[tool.mypy]
+mypy_path = "sage_validation"
+exclude = [
+    "venv",
+    "docs",
+    "manage.py",
+
+
+]
+ignore_missing_imports = true  # Ignore imports that are not typed
+disallow_untyped_calls = true    # Disallow calls to untyped functions
+disallow_untyped_defs = true      # Disallow untyped function definitions
+
+[tool.ruff]
+extend-exclude = [
+    "*/migrations",
+    "sage_validation/wsgi.py",
+    "sage_validation/asgi.py",
+    "sage_validation/settings.py",
+    "manage.py",
+    "docs",
+    "theme",
+]
+select = [
+    "A", "ARG", "B", "BLE", "C", "COM", "C4", "C90", "D", "DTZ",
+    "E", "EM", "ERA", "F", "FA", "FBT", "FLY", "FURB", "G", "I",
+    "ICN", "INP", "ISC", "LOG", "N", "PERF", "PGH", "PIE", "PL",
+    "PT", "PTH", "PYI", "Q", "RET", "R", "RET", "RSE", "RUF",
+    "S", "SIM", "SLF", "T", "T20", "TID", "TRY", "UP", "W", "YTT"
+]
+ignore = [
+    "COM812", "D203", "D213", "ISC001", "N805", "PLR0913", "PLR0904", "PLW1514", "D104"
+]
+target-version = "py312"
+line-length = 120
+
+[tool.ruff.lint.flake8-tidy-imports]
+ban-relative-imports = "all"
+
+
diff --git a/sage_validation/settings.py b/sage_validation/settings.py
index 410e0e3..ba5b964 100644
--- a/sage_validation/settings.py
+++ b/sage_validation/settings.py
@@ -24,7 +24,7 @@ SECRET_KEY = "django-insecure-9tdba&yktxzclzokj^=uxfsmisgeo8(6!p3koa8ndy8s^3x@@y
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = True
 
-ALLOWED_HOSTS = []
+ALLOWED_HOSTS: list[str] = []
 
 # Application definition
 INSTALLED_APPS = [
diff --git a/sage_validation/urls.py b/sage_validation/urls.py
index 7376ada..ce10540 100644
--- a/sage_validation/urls.py
+++ b/sage_validation/urls.py
@@ -1,19 +1,4 @@
-"""
-URL configuration for sage_validation project.
-
-The `urlpatterns` list routes URLs to views. For more information please see:
-    https://docs.djangoproject.com/en/5.1/topics/http/urls/
-Examples:
-Function views
-    1. Add an import:  from my_app import views
-    2. Add a URL to urlpatterns:  path('', views.home, name='home')
-Class-based views
-    1. Add an import:  from other_app.views import Home
-    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
-Including another URLconf
-    1. Import the include() function: from django.urls import include, path
-    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
-"""
+"""URL configuration for sage_validation project."""
 from django.contrib import admin
 from django.urls import path
 from django.urls.conf import include
diff --git a/setup.py b/setup.py
index 0154142..a180dff 100644
--- a/setup.py
+++ b/setup.py
@@ -1,17 +1,18 @@
-from setuptools import setup, find_packages
+"""Setup script for the sage-validation package."""
+from setuptools import find_packages, setup
 
 setup(
-    name='sage-validation',
-    version='0.1',
+    name="sage-validation",
+    version="0.1",
     packages=find_packages(),
     include_package_data=True,
     install_requires=[
-        'Django==5.1.1'
+        "Django==5.1.1"
     ],
     classifiers=[
-        'Programming Language :: Python :: 3',
-        'Framework :: Django',
-        'Operating System :: OS Independent',
+        "Programming Language :: Python :: 3",
+        "Framework :: Django",
+        "Operating System :: OS Independent",
     ],
-    python_requires='>=3.12'
+    python_requires=">=3.12"
 )
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..74a6ea7
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,10 @@
+[tox]
+envlist = py312
+
+[testenv]
+deps =
+    mypy
+    ruff
+commands =
+    ruff check .
+    mypy .
-- 
GitLab