Skip to content
Merged
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
99 changes: 78 additions & 21 deletions django_fastdev/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,56 @@ def get_gitignore_path():
return None


def get_venv_path():
"""
Retrieve the path to the active virtual environment, if any.

Returns:
str or None: The path to the virtual environment, or None if not in a virtual environment.
"""
# 1. check the VIRTUAL_ENV environment variable
venv_path = os.getenv("VIRTUAL_ENV")
if venv_path:
return os.path.abspath(venv_path)

# 2. check for `sys.real_prefix` (used by `virtualenv`)
if hasattr(sys, "real_prefix") and sys.real_prefix != sys.prefix:
return sys.real_prefix

# 3. compare `sys.base_prefix` with `sys.prefix` (used by `venv`)
if hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix:
return sys.prefix

# 4. look for `pyvenv.cfg` in parent directories of `sys.executable`
def find_pyvenv_cfg(start_path):
"""
Recursively search for `pyvenv.cfg` in the given directory and its parents.

Args:
start_path (str): The directory to start searching from.

Returns:
str or None: The path to `pyvenv.cfg` if found, or None otherwise.
"""
current_path = start_path
while True:
potential_cfg = os.path.join(current_path, "pyvenv.cfg")
if os.path.isfile(potential_cfg):
return potential_cfg
parent_path = os.path.dirname(current_path)
if parent_path == current_path:
break
current_path = parent_path
return None

venv_cfg_path = find_pyvenv_cfg(os.path.dirname(sys.executable))
if venv_cfg_path:
return os.path.dirname(venv_cfg_path)

# if all else fails, return None
return None


def is_absolute_url(url):
return bool(url.startswith('/') or url.startswith('http://') or url.startswith('https://'))

Expand Down Expand Up @@ -232,22 +282,29 @@ def is_from_project(cls):

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
# exit early if the module is built-in or dynamically created
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)
project_dir = get_path_for_django_project()
venv_dir = get_venv_path()

# check if the module belongs to the project directory
if not module_path.startswith(str(project_dir)):
return False

# exclude modules from the virtual environment if applicable
if venv_dir and module_path.startswith(str(venv_dir)):
return False

return True


def fastdev_ignore(target):
Expand All @@ -256,14 +313,6 @@ def fastdev_ignore(target):
return target


def get_venv_folder_name():
import os

path_to_venv = os.environ["VIRTUAL_ENV"]
venv_folder = os.path.basename(path_to_venv)
return venv_folder


@cache
def get_ignored_template_list():
ignored_templates_settings = getattr(settings, 'FASTDEV_IGNORED_TEMPLATES', [])
Expand Down Expand Up @@ -308,14 +357,15 @@ def resolve_override(self, context, ignore_failures=False, ignore_failures_for_r
if not strict_template_checking():
# worry only about templates inside our project dir; if they
# exist elsewhere, then go to standard django behavior
venv_dir = os.environ.get('VIRTUAL_ENV', '')
venv_dir = get_venv_path()
project_dir = get_path_for_django_project()
origin = context.template.origin.name
if (
origin != '<unknown source>' and
'django-fastdev/tests/' not in origin
origin != '<unknown source>'
and 'django-fastdev/tests/' not in origin
and (
not origin.startswith(str(settings.BASE_DIR))
or (venv_dir and origin.startswith(venv_dir))
not origin.startswith(str(project_dir))
or (bool(venv_dir) and origin.startswith(str(venv_dir)))
)
):
return orig_resolve(self, context, ignore_failures=ignore_failures)
Expand Down Expand Up @@ -409,12 +459,19 @@ def if_render_override(self, context):
def fastdev_full_clean(self):
orig_form_full_clean(self)
# 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):
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:
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:
Expand Down
8 changes: 8 additions & 0 deletions tests/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django import forms


class IgnoredForm(forms.Form):
my_field = forms.CharField()

def clean_nonexistent_field(self):
pass
19 changes: 16 additions & 3 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Form,
)

from django_fastdev.apps import InvalidCleanMethod
from django_fastdev.apps import InvalidCleanMethod, fastdev_ignore


def test_ok_form_works(settings):
Expand All @@ -30,7 +30,7 @@ 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)
# set strict mode otherwise test will fail (because dynamically typed form; doesn't have module.__file__ attribute)
settings.FASTDEV_STRICT_FORM_CHECKING = True
with pytest.raises(InvalidCleanMethod) as e:
MyForm().errors
Expand All @@ -57,9 +57,22 @@ 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)
# set strict mode otherwise test will fail (because dynamically typed form; doesn't have module.__file__ attribute)
settings.FASTDEV_STRICT_FORM_CHECKING = True
with pytest.raises(InvalidCleanMethod) as e:
MyForm().errors

assert str(e.value) == """Clean method clean_flield of class MyForm won't apply to any field. Available fields:\n\n field"""


def test_ignored_form_works(settings):
from .forms import IgnoredForm

IgnoredForm().errors

settings.DEBUG = True
with pytest.raises(InvalidCleanMethod) as e:
IgnoredForm().errors

IgnoredForm = fastdev_ignore(IgnoredForm)
IgnoredForm().errors
Loading