Repository: joshuadavidthomas/django-bird

Issue:
Template resolution inconsistent with documentation
In the docs, I have the template resolution for components written down as:

> When using {% bird accordion %}, the library will look for components in the following order:
> 
> 1. accordion/accordion.html
> 2. accordion/index.html
> 3. accordion.html

[Link](https://django-bird.readthedocs.io/en/latest/organization.html#real-world-example-accordion)

However, in the actual code, it's the other way around:

https://github.com/joshuadavidthomas/django-bird/blob/1e27e57463c28a6e5b896a0624373dda98bccd40/src/django_bird/templatetags/django_bird.py#L158-L163

https://github.com/joshuadavidthomas/django-bird/blob/1e27e57463c28a6e5b896a0624373dda98bccd40/src/django_bird/templatetags/django_bird.py#L138-L147

(In case the variable names are not clear enough out of context.)

I think the correct order is in the documentation. It should go specific -> general, so the actual implementation should be adjusted.

Also, should throw in a specific unit test for this, the one there right now doesn't test for the potential of multiple templates for the same component. [Link to test](https://github.com/joshuadavidthomas/django-bird/blob/1e27e57463c28a6e5b896a0624373dda98bccd40/tests/test_templatetags.py#L226).

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9070125..d3daf03 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,24 @@ and this project attempts to adhere to [Semantic Versioning](https://semver.org/
 
 ## [Unreleased]
 
+🚨 This release contains a breaking change. See the Changed section for more information. 🚨
+
+### Changed
+
+- Reversed template resolution order to prefer component-specific templates over generic ones.
+
+  For example, given a component named `button`, the previous resolution order was:
+
+  1. `button.html`
+  2. `button/button.html`
+  3. `button/index.html`
+
+  The new resolution order is:
+
+  1. `button/button.html`
+  2. `button/index.html`
+  3. `button.html`
+
 ## [0.1.1]
 
 ### Fixed
diff --git a/src/django_bird/templatetags/django_bird.py b/src/django_bird/templatetags/django_bird.py
index ef635ca..7c60d91 100644
--- a/src/django_bird/templatetags/django_bird.py
+++ b/src/django_bird/templatetags/django_bird.py
@@ -117,12 +117,12 @@ class BirdNode(template.Node):
         """
         Generate a list of potential template names for a component.
 
-        The function searches for templates in the following order:
-        1. In the base component directory, using the full component name
-        2. In a subdirectory named after the component
-            a. Using the full name (for subcomponents)
-            b. Using the last part of the name
-        3. In the same subdirectory, using a fallback 'index.html'
+        The function searches for templates in the following order (from most specific to most general):
+
+        1. In a subdirectory named after the component, using the component name
+        2. In the same subdirectory, using a fallback 'index.html'
+        3. In parent directory for nested components
+        4. In the base component directory, using the full component name
 
         The order of names is important as it determines the template resolution priority.
         This order allows for both direct matches and hierarchical component structures,
@@ -136,37 +136,34 @@ class BirdNode(template.Node):
 
         For example:
         - For an "input" component, the ordering would be:
-          1. `{component_dir}/input.html`
-          2. `{component_dir}/input/input.html`
-          3. `{component_dir}/input/index.html`
+            1. `{component_dir}/input/input.html`
+            2. `{component_dir}/input/index.html`
+            3. `{component_dir}/input.html`
         - For an "input.label" component:
-          1. `{component_dir}/input.label.html`
-          2. `{component_dir}/input/label.html`
-          3. `{component_dir}/input/label/label.html`
-          3. `{component_dir}/input/label/index.html`
+            1. `{component_dir}/input/label/label.html`
+            2. `{component_dir}/input/label/index.html`
+            3. `{component_dir}/input/label.html`
+            4. `{component_dir}/input.label.html`
 
         Returns:
             list[str]: A list of potential template names in resolution order.
         """
+        template_names = []
+        component_dirs = list(dict.fromkeys([*app_settings.COMPONENT_DIRS, "bird"]))
 
-        template_names: list[str] = []
+        name_parts = self.name.split(".")
+        path_name = "/".join(name_parts)
 
-        for component_dir in app_settings.COMPONENT_DIRS + ["bird"]:
-            name_parts = self.name.split(".")
-            path_name = "/".join(name_parts)
+        for component_dir in component_dirs:
             potential_names = [
-                f"{component_dir}/{self.name}.html",
-                f"{component_dir}/{path_name}.html",
                 f"{component_dir}/{path_name}/{name_parts[-1]}.html",
                 f"{component_dir}/{path_name}/index.html",
+                f"{component_dir}/{path_name}.html",
+                f"{component_dir}/{self.name}.html",
             ]
-            # Preserve order while ensuring uniqueness by removing duplicates.
-            # Using a set here would not keep the ordering which is important
-            # in template resolution.
-            ordered_unique_names = dict.fromkeys(potential_names)
-            template_names.extend(list(ordered_unique_names))
+            template_names.extend(potential_names)
 
-        return template_names
+        return list(dict.fromkeys(template_names))
 
 
 @register.tag("bird:slot")


Test patch:
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
index 08eaf85..a404106 100644
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -237,68 +237,70 @@ class TestBirdTemplateTagFutureFeatures:
 
 class TestBirdNode:
     @pytest.mark.parametrize(
-        "sub_dir,filename,component,nodename,expected",
+        "name,component_dirs,expected",
         [
             (
-                None,
                 "button",
-                "<button>Click me</button>",
-                "button",
-                "bird/button.html",
+                [],
+                [
+                    "bird/button/button.html",
+                    "bird/button/index.html",
+                    "bird/button.html",
+                ],
             ),
             (
-                "button",
-                "index",
-                "<button>Click me</button>",
-                "button",
-                "bird/button/index.html",
+                "input.label",
+                [],
+                [
+                    "bird/input/label/label.html",
+                    "bird/input/label/index.html",
+                    "bird/input/label.html",
+                    "bird/input.label.html",
+                ],
             ),
             (
                 "button",
-                "button",
-                "<button>Click me</button>",
-                "button",
-                "bird/button/button.html",
-            ),
-            (
-                None,
-                "button.label",
-                "<button>Click me</button>",
-                "button.label",
-                "bird/button.label.html",
-            ),
-            (
-                None,
-                "button.label",
-                "<button>Click me</button>",
-                "button.label",
-                "bird/button/label.html",
+                ["custom", "theme"],
+                [
+                    "custom/button/button.html",
+                    "custom/button/index.html",
+                    "custom/button.html",
+                    "theme/button/button.html",
+                    "theme/button/index.html",
+                    "theme/button.html",
+                    "bird/button/button.html",
+                    "bird/button/index.html",
+                    "bird/button.html",
+                ],
             ),
         ],
     )
-    def test_get_template_names(
-        self, sub_dir, filename, component, nodename, expected, create_bird_template
-    ):
-        create_bird_template(name=filename, content=component, sub_dir=sub_dir)
-        node = BirdNode(name=nodename, attrs=[], nodelist=None)
+    def test_get_template_names(self, name, component_dirs, expected):
+        node = BirdNode(name=name, attrs=[], nodelist=None)
 
-        template_names = node.get_template_names()
+        with override_settings(DJANGO_BIRD={"COMPONENT_DIRS": component_dirs}):
+            template_names = node.get_template_names()
 
-        assert any(expected in template_name for template_name in template_names)
+        assert template_names == expected
 
-    @override_settings(DJANGO_BIRD={"COMPONENT_DIRS": ["not_default"]})
-    def test_get_template_names_path_component_dirs(self, create_bird_template):
-        create_bird_template(
-            name="button",
-            content="<button class=btn_class>Click me</button>",
-            bird_dir_name="not_default",
-        )
-
-        node = BirdNode(name="button", attrs=[], nodelist=None)
+    def test_get_template_names_invalid(self):
+        node = BirdNode(name="input.label", attrs=[], nodelist=None)
 
         template_names = node.get_template_names()
 
-        assert any("not_default" in template_name for template_name in template_names)
+        assert "bird/input/label/invalid.html" not in template_names
+
+    def test_get_template_names_duplicates(self):
+        with override_settings(DJANGO_BIRD={"COMPONENT_DIRS": ["bird"]}):
+            node = BirdNode(name="button", attrs=[], nodelist=None)
+            template_names = node.get_template_names()
+
+            template_counts = {}
+            for template in template_names:
+                template_counts[template] = template_counts.get(template, 0) + 1
+
+            for template, count in template_counts.items():
+                assert count == 1
 
 
 class TestSlotsTemplateTag: