diff --git a/django/template/base.py b/django/template/base.py index 0f1eca58db..ee2e145c04 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -880,6 +880,10 @@ class Variable: try: # catch-all for silent variable failures for bit in self.lookups: try: # dictionary lookup + # Only allow if the metaclass implements __getitem__. See + # https://docs.python.org/3/reference/datamodel.html#classgetitem-versus-getitem + if not hasattr(type(current), "__getitem__"): + raise TypeError current = current[bit] # ValueError/IndexError are for numpy.array lookup on # numpy < 1.9 and 1.9+ respectively diff --git a/tests/template_tests/syntax_tests/test_basic.py b/tests/template_tests/syntax_tests/test_basic.py index 20bf30d55c..50e7a4c7b1 100644 --- a/tests/template_tests/syntax_tests/test_basic.py +++ b/tests/template_tests/syntax_tests/test_basic.py @@ -346,6 +346,52 @@ class BasicSyntaxTests(SimpleTestCase): output = self.engine.render_to_string("tpl-weird-percent") self.assertEqual(output, "% %s") + @setup( + {"template": "{{ class_var.class_property }} | {{ class_var.class_method }}"} + ) + def test_subscriptable_class(self): + class MyClass(list): + # As of Python 3.9 list defines __class_getitem__ which makes it + # subscriptable. + class_property = "Example property" + do_not_call_in_templates = True + + @classmethod + def class_method(cls): + return "Example method" + + for case in (MyClass, lambda: MyClass): + with self.subTest(case=case): + output = self.engine.render_to_string("template", {"class_var": case}) + self.assertEqual(output, "Example property | Example method") + + @setup({"template": "{{ meals.lunch }}"}) + def test_access_class_property_if_getitem_is_defined_in_metaclass(self): + """ + If the metaclass defines __getitem__, the template system should use + it to resolve the dot notation. + """ + + class MealMeta(type): + def __getitem__(cls, name): + return getattr(cls, name) + " is yummy." + + class Meals(metaclass=MealMeta): + lunch = "soup" + do_not_call_in_templates = True + + # Make class type subscriptable. + def __class_getitem__(cls, key): + from types import GenericAlias + + return GenericAlias(cls, key) + + self.assertEqual(Meals.lunch, "soup") + self.assertEqual(Meals["lunch"], "soup is yummy.") + + output = self.engine.render_to_string("template", {"meals": Meals}) + self.assertEqual(output, "soup is yummy.") + class BlockContextTests(SimpleTestCase): def test_repr(self):