From 060c979a27fce7c8dd028fa0020dd8832ad2d747 Mon Sep 17 00:00:00 2001 From: Naggafin Date: Wed, 25 Dec 2024 19:07:03 -0500 Subject: [PATCH 01/12] bug fix --- django_fastdev/apps.py | 49 +++++++++++++++++++++++++----------------- tests/forms.py | 8 +++++++ tests/test_forms.py | 19 +++++++++++++--- 3 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 tests/forms.py diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index df5210b..814c935 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -83,6 +83,15 @@ def get_gitignore_path(): return None +def get_venv_path(): + return os.environ.get("VIRTUAL_ENV", None) + + +def get_venv_folder_name(): + venv_path = get_venv_path() + return os.path.basename(venv_path) if venv_path else venv_path + + def is_absolute_url(url): return bool(url.startswith('/') or url.startswith('http://') or url.startswith('https://')) @@ -232,7 +241,6 @@ 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. @@ -243,11 +251,12 @@ def is_from_project(cls): if not module or not hasattr(module, "__file__"): return False - venv_dir = os.environ.get("VIRTUAL_ENV", "") + venv_dir = get_venv_path() + project_dir = get_path_for_django_project() module_path = os.path.abspath(module.__file__) - return module_path.startswith( - str(settings.BASE_DIR) - ) and not module_path.startswith(venv_dir) + return module_path.startswith(str(project_dir)) and not ( + bool(venv_dir) and module_path.startswith(venv_dir) + ) def fastdev_ignore(target): @@ -256,14 +265,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', []) @@ -308,14 +309,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 != '' and - 'django-fastdev/tests/' not in origin + origin != '' + 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(venv_dir)) ) ): return orig_resolve(self, context, ignore_failures=ignore_failures) @@ -409,12 +411,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: diff --git a/tests/forms.py b/tests/forms.py new file mode 100644 index 0000000..724f766 --- /dev/null +++ b/tests/forms.py @@ -0,0 +1,8 @@ +from django import forms + + +class IgnoredForm(forms.Form): + my_field = forms.CharField() + + def clean_nonexistent_field(self): + pass diff --git a/tests/test_forms.py b/tests/test_forms.py index cb50b9d..bf44607 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -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): @@ -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 @@ -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 From 8b32ef51e024d30273d8161144aa1225c7a574a7 Mon Sep 17 00:00:00 2001 From: Naggafin Date: Thu, 26 Dec 2024 17:29:35 -0500 Subject: [PATCH 02/12] reformatted for readability --- django_fastdev/apps.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index 814c935..31f7abc 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -254,9 +254,13 @@ def is_from_project(cls): venv_dir = get_venv_path() project_dir = get_path_for_django_project() module_path = os.path.abspath(module.__file__) - return module_path.startswith(str(project_dir)) and not ( - bool(venv_dir) and module_path.startswith(venv_dir) - ) + + if module_path.startswith(project_dir): + # check against venv after project dir matches, + # just in case venv resides in project dir + if venv_dir and module_path.startswith(venv_dir): + return False + return True def fastdev_ignore(target): From d342868161c77e8c0fa1b730e16e80813be8822b Mon Sep 17 00:00:00 2001 From: Naggafin Date: Thu, 26 Dec 2024 17:32:47 -0500 Subject: [PATCH 03/12] reformatted for readability --- django_fastdev/apps.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index 31f7abc..f619796 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -255,10 +255,10 @@ def is_from_project(cls): project_dir = get_path_for_django_project() module_path = os.path.abspath(module.__file__) - if module_path.startswith(project_dir): + if module_path.startswith(str(project_dir)): # check against venv after project dir matches, # just in case venv resides in project dir - if venv_dir and module_path.startswith(venv_dir): + if venv_dir and module_path.startswith(str(venv_dir)): return False return True @@ -321,7 +321,7 @@ def resolve_override(self, context, ignore_failures=False, ignore_failures_for_r and 'django-fastdev/tests/' not in origin and ( not origin.startswith(str(project_dir)) - or (bool(venv_dir) and origin.startswith(venv_dir)) + or (bool(venv_dir) and origin.startswith(str(venv_dir))) ) ): return orig_resolve(self, context, ignore_failures=ignore_failures) From 580b7e80ffe98c7621d04a58042f7ec84750ce7c Mon Sep 17 00:00:00 2001 From: Naggafin Date: Mon, 30 Dec 2024 19:26:48 -0500 Subject: [PATCH 04/12] fixed implicit return --- django_fastdev/apps.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index f619796..509377d 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -247,20 +247,23 @@ def is_from_project(cls): """ 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 = get_venv_path() - project_dir = get_path_for_django_project() module_path = os.path.abspath(module.__file__) + project_dir = get_path_for_django_project() + venv_dir = get_venv_path() - if module_path.startswith(str(project_dir)): - # check against venv after project dir matches, - # just in case venv resides in project dir - if venv_dir and module_path.startswith(str(venv_dir)): - return False - return True + # 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): From 75280a6e526863d88ce38078ef081869e69c8bbb Mon Sep 17 00:00:00 2001 From: Naggafin Date: Mon, 30 Dec 2024 19:55:39 -0500 Subject: [PATCH 05/12] updated get_venv_path() for reliability per suggestion by @danlamanna --- django_fastdev/apps.py | 50 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index 509377d..16cc19c 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -84,7 +84,55 @@ def get_gitignore_path(): def get_venv_path(): - return os.environ.get("VIRTUAL_ENV", None) + """ + 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` + 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 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 + def get_venv_folder_name(): From 3a7a9e69c06eaaa3db3f5044ed50d6d2075a9c5d Mon Sep 17 00:00:00 2001 From: Naggafin Date: Mon, 30 Dec 2024 20:08:24 -0500 Subject: [PATCH 06/12] forgot to remove the original declaration, oops --- django_fastdev/apps.py | 49 ++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index 16cc19c..db8281b 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -104,6 +104,27 @@ def get_venv_path(): 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) @@ -112,34 +133,6 @@ def get_venv_path(): return None -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 - - - -def get_venv_folder_name(): - venv_path = get_venv_path() - return os.path.basename(venv_path) if venv_path else venv_path - - def is_absolute_url(url): return bool(url.startswith('/') or url.startswith('http://') or url.startswith('https://')) From da85be0a861feaa484f82e5b6e2476c40b3a9329 Mon Sep 17 00:00:00 2001 From: naggafin Date: Fri, 2 May 2025 23:08:20 -0400 Subject: [PATCH 07/12] made fastdev template variable checks pass on |default_if_none --- django_fastdev/apps.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index db8281b..fa36559 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -350,10 +350,17 @@ def resolve_override(self, context, ignore_failures=False, ignore_failures_for_r if isinstance(self.var, Variable): try: - self.var.resolve(context) + return self.var.resolve(context) except FastDevVariableDoesNotExist: raise except VariableDoesNotExist as e: + # If the filter includes default_if_none, suppress the exception and return None + if any( + filter_func.func.__name__ == 'default_if_none' + for filter_name, filter_func in self.filters + ): + return None + if not strict_template_checking(): # worry only about templates inside our project dir; if they # exist elsewhere, then go to standard django behavior From 9c0ed4583b1a005ab13b2c4bd2bedf21b19f33d1 Mon Sep 17 00:00:00 2001 From: naggafin Date: Sat, 3 May 2025 21:23:24 -0400 Subject: [PATCH 08/12] also made fastdev work with |default --- django_fastdev/apps.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index fa36559..b27ffc3 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -35,6 +35,10 @@ IfNode, ForNode ) +from django.template.defaultfilters import ( + default, + default_if_none, +) from django.template.loader_tags import ( BlockNode, ExtendsNode, @@ -350,14 +354,15 @@ def resolve_override(self, context, ignore_failures=False, ignore_failures_for_r if isinstance(self.var, Variable): try: - return self.var.resolve(context) + self.var.resolve(context) except FastDevVariableDoesNotExist: raise except VariableDoesNotExist as e: - # If the filter includes default_if_none, suppress the exception and return None + # If the filter includes default or default_if_none, suppress + # the exception and return None if any( - filter_func.func.__name__ == 'default_if_none' - for filter_name, filter_func in self.filters + filter in (default, default_if_none) + for filter, args in self.filters ): return None From 7bd0d3e44b19f30886be40b6d031ad81433ca8d5 Mon Sep 17 00:00:00 2001 From: naggafin Date: Sat, 3 May 2025 21:43:56 -0400 Subject: [PATCH 09/12] updated documentation --- README.rst | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 90c981d..8c0472a 100644 --- a/README.rst +++ b/README.rst @@ -10,13 +10,13 @@ Features Error on non-existent template variables ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Django templates by default hide errors, and when it does show an error it's often not very helpful. This app will change this so that if you do: +Django templates by default hide errors, and when it does show an error it's often not very helpful. This app changes this behavior to provide more informative error messages. For example, if you use: .. code:: html {{ does_not_exist }} -instead of rendering that as an empty string, this app will give you an error message: +instead of rendering that as an empty string, this app will raise a :code:`FastDevVariableDoesNotExist` error with a detailed message: .. code:: @@ -34,11 +34,22 @@ instead of rendering that as an empty string, this app will give you an error me request user -There are more specialized error messages for when you try to access the contents of a :code:`dict`, and attributes of an object a few levels deep like :code:`foo.bar.baz` (where baz doesn't exist). +There are more specialized error messages for accessing non-existent keys in a :code:`dict` or attributes of an object several levels deep, such as :code:`foo.bar.baz` (where :code:`baz` doesn't exist). -By default, :code:`django-fastdev` only checks templates that exist within your project directory. If you want it to check ALL templates, including stock django templates and templates from third party libraries, add :code:`FASTDEV_STRICT_TEMPLATE_CHECKING = True` to your project :code:`settings.py`. +**Handling `default` and `default_if_none` Filters** -Variable access inside :code:`{% if %}` will not crash unless the setting :code:`FASTDEV_STRICT_IF` is set to :code:`True`. If you use this setting and want to check for existence of a variable, use :code:`{% ifexists %}` from the fastdev template tag library. +When using the :code:`default` or :code:`default_if_none` filters, :code:`django-fastdev` will not raise an exception for non-existent variables. Instead, it behaves as one might intuitively expect by populating the context variable with the result of the filter operation. For example: + +.. code:: html + + {{ does_not_exist|default:"N/A" }} + {{ does_not_exist|default_if_none:"" }} + +In these cases: +- If :code:`does_not_exist` is undefined, :code:`default:"N/A"` will render as :code:`N/A`, and :code:`default_if_none:""` will render as an empty string (:code:`""`). +- This ensures that templates using these filters handle missing variables gracefully, aligning with Django's built-in behavior while maintaining :code:`django-fastdev`'s strict checking for other cases. + +By default, :code:`django-fastdev` only checks templates that exist within your project directory. To check ALL templates, including stock Django templates and templates from third-party libraries, add :code:`FASTDEV_STRICT_TEMPLATE_CHECKING = True` to your project :code:`settings.py`. Improved TemplateDoesNotExist errors @@ -46,6 +57,7 @@ Improved TemplateDoesNotExist errors Good suggestions for what you wanted to do, and a complete list of all valid values makes it very easy to fix `TemplateDoesNotExist` errors. + NoReverseMatch errors ~~~~~~~~~~~~~~~~~~~~~ @@ -105,6 +117,7 @@ Django will silently throw away `hello!` because you wrote :code:`contents` inst of :code:`content`. :code:`django-fastdev` will turn this into an error which lists the invalid and valid block names in alphabetical order. + Better error messages for reverse ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 0d37cec19a45080dba0493b9b65f5e562587bc06 Mon Sep 17 00:00:00 2001 From: naggafin Date: Sat, 3 May 2025 21:47:54 -0400 Subject: [PATCH 10/12] updated documentation - redo --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 8c0472a..06649bb 100644 --- a/README.rst +++ b/README.rst @@ -46,8 +46,8 @@ When using the :code:`default` or :code:`default_if_none` filters, :code:`django {{ does_not_exist|default_if_none:"" }} In these cases: -- If :code:`does_not_exist` is undefined, :code:`default:"N/A"` will render as :code:`N/A`, and :code:`default_if_none:""` will render as an empty string (:code:`""`). -- This ensures that templates using these filters handle missing variables gracefully, aligning with Django's built-in behavior while maintaining :code:`django-fastdev`'s strict checking for other cases. + * If :code:`does_not_exist` is undefined, :code:`default:"N/A"` will render as :code:`N/A`, and :code:`default_if_none:""` will render as an empty string (:code:`""`). + * This ensures that templates using these filters handle missing variables gracefully, aligning with Django's built-in behavior while maintaining :code:`django-fastdev`'s strict checking for other cases. By default, :code:`django-fastdev` only checks templates that exist within your project directory. To check ALL templates, including stock Django templates and templates from third-party libraries, add :code:`FASTDEV_STRICT_TEMPLATE_CHECKING = True` to your project :code:`settings.py`. From 33f2c4546362f853f9e7ccd3a5b79758978a2067 Mon Sep 17 00:00:00 2001 From: naggafin Date: Tue, 13 May 2025 20:00:57 -0400 Subject: [PATCH 11/12] initial tests --- tests/test_template_variable_resolution.py | 77 ++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/test_template_variable_resolution.py diff --git a/tests/test_template_variable_resolution.py b/tests/test_template_variable_resolution.py new file mode 100644 index 0000000..6021885 --- /dev/null +++ b/tests/test_template_variable_resolution.py @@ -0,0 +1,77 @@ +from django.template import Context, Template +from django.test import TestCase +from django_fastdev.apps import FastDevVariableDoesNotExist +from unittest.mock import patch + + +class TestFastDevVariableResolution(TestCase): + def setUp(self): + self.context = Context({"existing_var": "test_value"}) + + def test_nonexistent_variable_with_default_filter(self): + template = Template('{{ nonexistent_var|default:"fallback" }}') + result = template.render(self.context) + self.assertEqual( + result, "fallback", "Expected fallback value for None with default filter" + ) + + def test_nonexistent_variable_with_default_if_none_filter(self): + template = Template('{{ nonexistent_var|default_if_none:"fallback" }}') + result = template.render(self.context) + self.assertEqual( + result, + "fallback", + "Expected fallback value for None with default_if_none filter", + ) + + def test_nonexistent_variable_without_filters(self): + template = Template("{{ nonexistent_var }}") + with self.assertRaises(FastDevVariableDoesNotExist) as cm: + template.render(self.context) + self.assertIn("nonexistent_var does not exist in context", str(cm.exception)) + + def test_existing_variable_with_default_filter(self): + template = Template('{{ existing_var|default:"fallback" }}') + result = template.render(self.context) + self.assertEqual( + result, "test_value", "Expected existing variable value with default filter" + ) + + def test_existing_variable_with_default_if_none_filter(self): + """Test that an existing variable with |default_if_none filter returns its value.""" + template = Template('{{ existing_var|default_if_none:"fallback" }}') + result = template.render(self.context) + self.assertEqual( + result, + "test_value", + "Expected existing variable value with default_if_none filter", + ) + + def test_nested_nonexistent_variable_with_default_filter(self): + template = Template('{{ obj.nonexistent_field|default:"fallback" }}') + context = Context({"obj": {"existing_field": "value"}}) + result = template.render(context) + self.assertEqual( + result, + "fallback", + "Expected fallback value for None with nested default filter", + ) + + @patch('django_fastdev.apps.template_is_ignored', lambda *args, **kwargs: True) + def test_ignored_template_behavior(self): + from django_fastdev.apps import template_is_ignored + self.assertTrue(template_is_ignored(), 'Mock patch of template_is_ignored failed') + template = Template('{{ nonexistent_var|default:"fallback" }}') + result = template.render(self.context) + self.assertEqual( + result, "fallback", "Expected Django default behavior for ignored template" + ) + + def test_nonexistent_variable_with_multiple_filters(self): + template = Template('{{ nonexistent_var|upper|default:"fallback" }}') + result = template.render(self.context) + self.assertEqual( + result, + "fallback", + "Expected fallback value for None with multiple filters including default", + ) From b5f723944bca0161e46880303fca7dfbf0269808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Hovm=C3=B6ller?= Date: Thu, 15 May 2025 09:12:42 +0200 Subject: [PATCH 12/12] default_if_none should not ignore errors. Also changed to pytest style tests. --- django_fastdev/apps.py | 4 +- .../test_ignored_template_with_filter.html | 1 + tests/test_template_variable_resolution.py | 130 ++++++++---------- 3 files changed, 63 insertions(+), 72 deletions(-) create mode 100644 tests/templates/ignored/test_ignored_template_with_filter.html diff --git a/django_fastdev/apps.py b/django_fastdev/apps.py index e73f796..3cf32fd 100644 --- a/django_fastdev/apps.py +++ b/django_fastdev/apps.py @@ -361,10 +361,10 @@ def resolve_override(self, context, ignore_failures=False, ignore_failures_for_r # If the filter includes default or default_if_none, suppress # the exception and return None if any( - filter in (default, default_if_none) + filter == default for filter, args in self.filters ): - return None + return orig_resolve(self, context) if not strict_template_checking(): # worry only about templates inside our project dir; if they diff --git a/tests/templates/ignored/test_ignored_template_with_filter.html b/tests/templates/ignored/test_ignored_template_with_filter.html new file mode 100644 index 0000000..b759dec --- /dev/null +++ b/tests/templates/ignored/test_ignored_template_with_filter.html @@ -0,0 +1 @@ +{{ nonexistent_var|default:"fallback" }} diff --git a/tests/test_template_variable_resolution.py b/tests/test_template_variable_resolution.py index 6021885..b53b120 100644 --- a/tests/test_template_variable_resolution.py +++ b/tests/test_template_variable_resolution.py @@ -1,77 +1,67 @@ +import pytest from django.template import Context, Template +from django.template.loader import get_template from django.test import TestCase from django_fastdev.apps import FastDevVariableDoesNotExist from unittest.mock import patch -class TestFastDevVariableResolution(TestCase): - def setUp(self): - self.context = Context({"existing_var": "test_value"}) - - def test_nonexistent_variable_with_default_filter(self): - template = Template('{{ nonexistent_var|default:"fallback" }}') - result = template.render(self.context) - self.assertEqual( - result, "fallback", "Expected fallback value for None with default filter" - ) - - def test_nonexistent_variable_with_default_if_none_filter(self): - template = Template('{{ nonexistent_var|default_if_none:"fallback" }}') - result = template.render(self.context) - self.assertEqual( - result, - "fallback", - "Expected fallback value for None with default_if_none filter", - ) - - def test_nonexistent_variable_without_filters(self): - template = Template("{{ nonexistent_var }}") - with self.assertRaises(FastDevVariableDoesNotExist) as cm: - template.render(self.context) - self.assertIn("nonexistent_var does not exist in context", str(cm.exception)) - - def test_existing_variable_with_default_filter(self): - template = Template('{{ existing_var|default:"fallback" }}') - result = template.render(self.context) - self.assertEqual( - result, "test_value", "Expected existing variable value with default filter" - ) - - def test_existing_variable_with_default_if_none_filter(self): - """Test that an existing variable with |default_if_none filter returns its value.""" - template = Template('{{ existing_var|default_if_none:"fallback" }}') - result = template.render(self.context) - self.assertEqual( - result, - "test_value", - "Expected existing variable value with default_if_none filter", - ) - - def test_nested_nonexistent_variable_with_default_filter(self): - template = Template('{{ obj.nonexistent_field|default:"fallback" }}') - context = Context({"obj": {"existing_field": "value"}}) +context = Context({"existing_var": "test_value"}) + + +def test_nonexistent_variable_with_default_filter(): + template = Template('{{ nonexistent_var|default:"fallback" }}') + result = template.render(context) + assert result == "fallback", "Expected fallback value for None with default filter" + + +def test_nonexistent_variable_with_default_if_none_filter(): + template = Template('{{ nonexistent_var|default_if_none:"fallback" }}') + with pytest.raises(FastDevVariableDoesNotExist) as cm: result = template.render(context) - self.assertEqual( - result, - "fallback", - "Expected fallback value for None with nested default filter", - ) - - @patch('django_fastdev.apps.template_is_ignored', lambda *args, **kwargs: True) - def test_ignored_template_behavior(self): - from django_fastdev.apps import template_is_ignored - self.assertTrue(template_is_ignored(), 'Mock patch of template_is_ignored failed') - template = Template('{{ nonexistent_var|default:"fallback" }}') - result = template.render(self.context) - self.assertEqual( - result, "fallback", "Expected Django default behavior for ignored template" - ) - - def test_nonexistent_variable_with_multiple_filters(self): - template = Template('{{ nonexistent_var|upper|default:"fallback" }}') - result = template.render(self.context) - self.assertEqual( - result, - "fallback", - "Expected fallback value for None with multiple filters including default", - ) + assert "nonexistent_var does not exist in context" in str(cm.value) + + +def test_nonexistent_variable_without_filters(): + template = Template("{{ nonexistent_var }}") + with pytest.raises(FastDevVariableDoesNotExist) as cm: + template.render(context) + assert "nonexistent_var does not exist in context" in str(cm.value) + + +def test_existing_variable_with_default_filter(): + template = Template('{{ existing_var|default:"fallback" }}') + result = template.render(context) + assert result == "test_value", "Expected existing variable value with default filter" + + +def test_existing_variable_with_default_if_none_filter(): + """Test that an existing variable with |default_if_none filter returns its value.""" + template = Template('{{ existing_var|default_if_none:"fallback" }}') + result = template.render(context) + assert result == "test_value", "Expected existing variable value with default_if_none filter" + + +def test_nested_nonexistent_variable_with_default_filter(): + template = Template('{{ obj.nonexistent_field|default:"fallback" }}') + context = Context({"obj": {"existing_field": "value"}}) + result = template.render(context) + assert result == "fallback", "Expected fallback value for None with nested default filter" + + +def test_ignored_template_with_filter(): + template = get_template('ignored/test_ignored_template_with_filter.html') + result = template.render().strip() + assert result == "fallback", "Expected Django default behavior for ignored template" + + +def test_nonexistent_variable_with_multiple_filters(): + template = Template('{{ nonexistent_var|upper|default:"fallback" }}') + result = template.render(context) + assert result == "fallback", "Expected fallback value for None with multiple filters including default" + + +def test_nonexistent_variable_with_multiple_filters2(): + template = Template('{{ nonexistent_var|default:"fallback"|upper }}') + result = template.render(context) + assert result == "FALLBACK", "Expected fallback value for None with multiple filters including default"