Skip to content
6 changes: 6 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Changelog
---------

1.14.0
~~~~~~

* Validation for Form clean() methods is now limited only to Forms existing within the project, and does not extend to third-party libraries


1.13.0
~~~~~~

Expand Down
5 changes: 5 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ an error message telling you that your clean method doesn't match anything.
This is also very useful during refactoring. Renaming a field is a lot safer
as if you forget to rename the clean method :code:`django-fastdev` will tell you!

By default, :code:`django-fastdev` will check only forms that exist within your project,
and not third-party libraries. If you would like to enable stricter validation that will
extend to ALL forms, you can set this by configuring :code:`FASTDEV_STRICT_FORM_CHECKING`
to :code:`True` in your Django settings.


Faster startup
~~~~~~~~~~~~~~
Expand Down
4 changes: 3 additions & 1 deletion django_fastdev/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '1.13.0'
__version__ = '1.14.0'

from threading import Thread
from time import sleep
Expand All @@ -8,6 +8,8 @@

from django.core.management.commands.runserver import Command

from .apps import fastdev_ignore # noqa

orig_check = Command.check
orig_check_migrations = Command.check_migrations

Expand Down
53 changes: 45 additions & 8 deletions django_fastdev/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import sys
import threading
from functools import cache
from inspect import getmodule
from typing import Optional
import warnings
from contextlib import (
Expand Down Expand Up @@ -221,6 +222,40 @@ def strict_template_checking():
return getattr(settings, 'FASTDEV_STRICT_TEMPLATE_CHECKING', False)


def strict_form_checking():
return getattr(settings, "FASTDEV_STRICT_FORM_CHECKING", False)


def is_from_project(cls):
"""
Check if a class originates from the project directory.

Args:
cls: The class to check.
project_root: The root directory of your project (absolute path).

Returns:
bool: True if the class originates from the project directory, False otherwise.
"""
module = getmodule(cls)

# check if built-in module or dynamically created class
if not module or not hasattr(module, "__file__"):
return False

venv_dir = os.environ.get("VIRTUAL_ENV", "")
module_path = os.path.abspath(module.__file__)
return module_path.startswith(
str(settings.BASE_DIR)
) and not module_path.startswith(venv_dir)


def fastdev_ignore(target):
"""A decorator to exclude a function or class from fastdev checks."""
setattr(target, "fastdev_ignore", True)
return target


def get_venv_folder_name():
import os

Expand Down Expand Up @@ -373,14 +408,16 @@ def if_render_override(self, context):

def fastdev_full_clean(self):
orig_form_full_clean(self)
from django.conf import settings
if settings.DEBUG:
prefix = 'clean_'
for name in dir(self):
if name.startswith(prefix) and callable(getattr(self, name)) and name[len(prefix):] not in self.fields:
fields = '\n '.join(sorted(self.fields.keys()))
# check if class is from our project, or strict form checking is enabled
if is_from_project(type(self)) or strict_form_checking() and not getattr(self, 'fastdev_ignore', False):
from django.conf import settings
if settings.DEBUG:
prefix = 'clean_'
for name in dir(self):
if name.startswith(prefix) and callable(getattr(self, name)) and name[len(prefix):] not in self.fields:
fields = '\n '.join(sorted(self.fields.keys()))

raise InvalidCleanMethod(f"""Clean method {name} of class {self.__class__.__name__} won't apply to any field. Available fields:
raise InvalidCleanMethod(f"""Clean method {name} of class {self.__class__.__name__} won't apply to any field. Available fields:

{fields}""")

Expand Down Expand Up @@ -574,7 +611,7 @@ def get_all_templates():
return template_list


from django.template import TemplateDoesNotExist
from django.template import TemplateDoesNotExist # noqa: E402


def fastdev_template_does_not_exist_error(self):
Expand Down
4 changes: 4 additions & 0 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def clean_flield(self):
MyForm().errors

settings.DEBUG = True
# set strict mode otherwise test will fail (because dynamically type form; doesn't exist in module)
settings.FASTDEV_STRICT_FORM_CHECKING = True
with pytest.raises(InvalidCleanMethod) as e:
MyForm().errors

Expand All @@ -55,6 +57,8 @@ def clean_flield(self):
MyForm().errors

settings.DEBUG = True
# set strict mode otherwise test will fail (because dynamically type form; doesn't exist in module)
settings.FASTDEV_STRICT_FORM_CHECKING = True
with pytest.raises(InvalidCleanMethod) as e:
MyForm().errors

Expand Down
Loading