diff --git a/HISTORY.rst b/HISTORY.rst index 761a0cc..2500c1c 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -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 ~~~~~~ diff --git a/README.rst b/README.rst index 3fd4a36..2e52520 100644 --- a/README.rst +++ b/README.rst @@ -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 ~~~~~~~~~~~~~~ diff --git a/django_fastdev/__init__.py b/django_fastdev/__init__.py index 06cfbe8..54a963c 100644 --- a/django_fastdev/__init__.py +++ b/django_fastdev/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.13.0' +__version__ = '1.14.0' from threading import Thread from time import sleep @@ -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 diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index c66dce4..df5210b 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -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 ( @@ -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 @@ -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}""") @@ -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): diff --git a/tests/test_forms.py b/tests/test_forms.py index 49a1d40..cb50b9d 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -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 @@ -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