From bd59f64c82bb666321003cb94f6252c2cc9f58e8 Mon Sep 17 00:00:00 2001 From: Till Heistermann Date: Mon, 27 Sep 2021 17:39:21 +0200 Subject: [PATCH 1/2] Add failing regression tests to reproduce bug --- test_spec.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/test_spec.py b/test_spec.py index 905e105..da0c724 100755 --- a/test_spec.py +++ b/test_spec.py @@ -466,6 +466,32 @@ def test_namedtuple_data(self): self.assertEqual(result, expected) + # Regression test for + # https://github.com/noahmorrison/chevron/issues/104 + def test_string_method_names_in_sections_1(self): + args = { + 'template': '{{#upper}}{{{.}}} == {{{upper}}}{{/upper}}', + 'data': {'upper': 'foo'}, + } + + result = chevron.render(**args) + expected = 'foo == foo' + self.assertEqual(result, expected) + + # Another regression test for + # https://github.com/noahmorrison/chevron/issues/104 + def test_string_method_names_in_sections_2(self): + args = { + 'template': ( + '{{#a}}{{#b}}{{{b}}} ' + '{{#upper}}{{{upper}}}{{/upper}}{{/b}}{{/a}}' + ), + 'data': {'a': [{'upper': 'foo', 'b': 'x'}]}, + } + result = chevron.render(**args) + expected = 'x foo' + self.assertEqual(result, expected) + def test_get_key_not_in_dunder_dict_returns_attribute(self): class C: foo = "bar" From 6ed704ed8e2a231e7d78f9788eae7a0eada20216 Mon Sep 17 00:00:00 2001 From: Till Heistermann Date: Mon, 27 Sep 2021 19:21:44 +0200 Subject: [PATCH 2/2] Be more restrictive with duck typing to avoid returning class attributes of strings --- chevron/renderer.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/chevron/renderer.py b/chevron/renderer.py index 65a00f6..796dfb7 100644 --- a/chevron/renderer.py +++ b/chevron/renderer.py @@ -48,6 +48,15 @@ def _html_escape(string): return string +def _is_instance_of_namedtuple(obj): + """Tests if an object is a namedtuple""" + return ( + isinstance(obj, tuple) and + hasattr(obj, '_asdict') and + hasattr(obj, '_fields') + ) + + def _get_key(key, scopes, warn, keep, def_ldel, def_rdel): """Get a key from the current scope""" @@ -61,16 +70,16 @@ def _get_key(key, scopes, warn, keep, def_ldel, def_rdel): try: # For every dot seperated key for child in key.split('.'): - # Move into the scope - try: - # Try subscripting (Normal dictionaries) + + # use different methods of access depending on the data structure + # passed: + if isinstance(scope, dict): scope = scope[child] - except (TypeError, AttributeError): - try: - scope = getattr(scope, child) - except (TypeError, AttributeError): - # Try as a list - scope = scope[int(child)] + elif hasattr(scope, "__dict__") or _is_instance_of_namedtuple(scope): + scope = getattr(scope, child) + else: + # Otherwise, try accessing as a list/tuple + scope = scope[int(child)] # Return an empty string if falsy, with two exceptions # 0 should return 0, and False should return False