From a087c1ef5cac5d5935d492fa8bdadb5bcf87db93 Mon Sep 17 00:00:00 2001 From: zer0def Date: Sun, 21 Oct 2018 21:29:28 +0200 Subject: [PATCH 1/4] Added `method_call` jinja filter. --- salt/utils/jinja.py | 5 +++++ tests/unit/utils/test_jinja.py | 5 ----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/salt/utils/jinja.py b/salt/utils/jinja.py index 29b208568d14..378c600f03b1 100644 --- a/salt/utils/jinja.py +++ b/salt/utils/jinja.py @@ -670,6 +670,11 @@ def symmetric_difference(lst1, lst2): ) +@jinja_filter("method_call") +def method_call(obj, f_name, *f_args, **f_kwargs): + return getattr(obj, f_name, lambda *args, **kwargs: None)(*f_args, **f_kwargs) + + @jinja2.contextfunction def show_full_context(ctx): return salt.utils.data.simple_types_filter( diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py index 8a6b57b18469..b9781cef765d 100644 --- a/tests/unit/utils/test_jinja.py +++ b/tests/unit/utils/test_jinja.py @@ -2,7 +2,6 @@ """ Tests for salt.utils.jinja """ -# Import Python libs from __future__ import absolute_import, print_function, unicode_literals import ast @@ -13,7 +12,6 @@ import re import tempfile -# Import Salt libs import salt.config import salt.loader @@ -39,12 +37,9 @@ from tests.support.case import ModuleCase from tests.support.helpers import flaky from tests.support.mock import MagicMock, Mock, patch - -# Import Salt Testing libs from tests.support.runtests import RUNTIME_VARS from tests.support.unit import TestCase, skipIf -# Import 3rd party libs try: import timelib # pylint: disable=W0611 From 2d518b92877e0e87a6b7667b47c8b3b03dcd9c28 Mon Sep 17 00:00:00 2001 From: zer0def Date: Mon, 22 Oct 2018 12:25:53 +0200 Subject: [PATCH 2/4] Added brief documentation and unit test. --- doc/topics/jinja/index.rst | 50 ++++++++++++++++++++++++++++++++++ tests/unit/utils/test_jinja.py | 27 ++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/doc/topics/jinja/index.rst b/doc/topics/jinja/index.rst index ac5ad7a5979d..0e7d1989f7c3 100644 --- a/doc/topics/jinja/index.rst +++ b/doc/topics/jinja/index.rst @@ -649,6 +649,56 @@ Returns: 1, 4 +.. jinja_ref:: method_call + +``method_call`` +--------------- + +.. versionadded:: develop + +Returns a result of object's method call. + +Example #1: + +.. code-block:: jinja + + {{ [1, 2, 1, 3, 4] | method_call('index', 1, 1, 3) }} + +Returns: + +.. code-block:: text + + 2 + +This filter can be used with the `map filter`_ to apply object methods without +using loop constructs or temporary variables. + +Example #2: + +.. code-block:: jinja + + {% set host_list = ['web01.example.com', 'db01.example.com'] %} + {% set host_list_split = [] %} + {% for item in host_list %} + {% do host_list_split.append(item.split('.', 1)) %} + {% endfor %} + {{ host_list_split }} + +Example #3: + +.. code-block:: jinja + + {{ host_list|map('method_call', 'split', '.', 1)|list }} + +Return of examples #2 and #3: + +.. code-block:: text + + [[web01, example.com], [db01, example.com]] + +.. _`map filter`: http://jinja.pocoo.org/docs/2.10/templates/#map + + .. jinja_ref:: is_sorted ``is_sorted`` diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py index b9781cef765d..82ad50a6a4d7 100644 --- a/tests/unit/utils/test_jinja.py +++ b/tests/unit/utils/test_jinja.py @@ -1571,6 +1571,33 @@ def test_symmetric_difference(self): ) self.assertEqual(rendered, "1, 4") + def test_method_call(self): + ''' + Test the `method_call` Jinja filter. + ''' + rendered = render_jinja_tmpl("{{ 6|method_call('bit_length') }}", + dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + self.assertEqual(rendered, "3") + rendered = render_jinja_tmpl("{{ 6.7|method_call('is_integer') }}", + dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + self.assertEqual(rendered, "False") + rendered = render_jinja_tmpl("{{ 'absaltba'|method_call('strip', 'ab') }}", + dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + self.assertEqual(rendered, "salt") + rendered = render_jinja_tmpl("{{ [1, 2, 1, 3, 4]|method_call('index', 1, 1, 3) }}", + dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + self.assertEqual(rendered, "2") + + # have to use `dictsort` to keep test result deterministic + rendered = render_jinja_tmpl("{{ {}|method_call('fromkeys', ['a', 'b', 'c'], 0)|dictsort }}", + dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + self.assertEqual(rendered, "[('a', 0), ('b', 0), ('c', 0)]") + + # missing object method test + rendered = render_jinja_tmpl("{{ 6|method_call('bit_width') }}", + dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + self.assertEqual(rendered, "None") + def test_md5(self): """ Test the `md5` Jinja filter. From b3bdcb833942a0898031b8fd50a0b6978976a37e Mon Sep 17 00:00:00 2001 From: Mike Place Date: Tue, 23 Oct 2018 08:40:20 -0600 Subject: [PATCH 3/4] Change versionadded Per @rallytime review --- doc/topics/jinja/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/topics/jinja/index.rst b/doc/topics/jinja/index.rst index 0e7d1989f7c3..95fa2e2d755a 100644 --- a/doc/topics/jinja/index.rst +++ b/doc/topics/jinja/index.rst @@ -87,7 +87,7 @@ the context into the included file is required: .. code-block:: jinja {% from 'lib.sls' import test with context %} - + Includes must use full paths, like so: .. code-block:: jinja @@ -654,7 +654,7 @@ Returns: ``method_call`` --------------- -.. versionadded:: develop +.. versionadded:: Sodium Returns a result of object's method call. From bbc706a890885ca45c1c0834d458be96bfd74c90 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Tue, 21 Apr 2020 12:34:22 +0100 Subject: [PATCH 4/4] Remove Py2 specific tests. Improve test class setup/teardown. --- tests/unit/utils/test_jinja.py | 170 ++++++++++++++------------------- 1 file changed, 71 insertions(+), 99 deletions(-) diff --git a/tests/unit/utils/test_jinja.py b/tests/unit/utils/test_jinja.py index 82ad50a6a4d7..5bc0cff51536 100644 --- a/tests/unit/utils/test_jinja.py +++ b/tests/unit/utils/test_jinja.py @@ -122,6 +122,7 @@ def setUp(self): def tearDown(self): salt.utils.files.rm_rf(self.tempdir) + self.tempdir = self.template_dir = self.opts def test_searchpath(self): """ @@ -279,6 +280,7 @@ def setUp(self): def tearDown(self): salt.utils.files.rm_rf(self.tempdir) + self.tempdir = self.template_dir = self.local_opts = self.local_salt = None def test_fallback(self): """ @@ -554,19 +556,6 @@ def test_render_with_syntax_error(self): dict(opts=self.local_opts, saltenv="test", salt=self.local_salt), ) - @skipIf(six.PY3, "Not applicable to Python 3") - def test_render_with_unicode_syntax_error(self): - with patch.object(builtins, "__salt_system_encoding__", "utf-8"): - template = "hello\n\n{{ bad\n\nfoo한" - expected = r".*---\nhello\n\n{{ bad\n\nfoo\xed\x95\x9c <======================\n---" - self.assertRaisesRegex( - SaltRenderError, - expected, - render_jinja_tmpl, - template, - dict(opts=self.local_opts, saltenv="test", salt=self.local_salt), - ) - def test_render_with_utf8_syntax_error(self): with patch.object(builtins, "__salt_system_encoding__", "utf-8"): template = "hello\n\n{{ bad\n\nfoo한" @@ -616,9 +605,9 @@ def test_render_with_undefined_variable_unicode(self): class TestJinjaDefaultOptions(TestCase): - def __init__(self, *args, **kws): - TestCase.__init__(self, *args, **kws) - self.local_opts = { + @classmethod + def setUpClass(cls): + cls.local_opts = { "cachedir": os.path.join(RUNTIME_VARS.TMP, "jinja-template-cache"), "file_buffer_size": 1048576, "file_client": "local", @@ -637,11 +626,15 @@ def __init__(self, *args, **kws): ), "jinja_env": {"line_comment_prefix": "##", "line_statement_prefix": "%"}, } - self.local_salt = { + cls.local_salt = { "myvar": "zero", "mylist": [0, 1, 2, 3], } + @classmethod + def tearDownClass(cls): + cls.local_opts = cls.local_salt = None + def test_comment_prefix(self): template = """ @@ -676,9 +669,9 @@ def test_statement_prefix(self): class TestCustomExtensions(TestCase): - def __init__(self, *args, **kws): - super(TestCustomExtensions, self).__init__(*args, **kws) - self.local_opts = { + @classmethod + def setUpClass(cls): + cls.local_opts = { "cachedir": os.path.join(RUNTIME_VARS.TMP, "jinja-template-cache"), "file_buffer_size": 1048576, "file_client": "local", @@ -696,7 +689,7 @@ def __init__(self, *args, **kws): os.path.dirname(os.path.abspath(__file__)), "extmods" ), } - self.local_salt = { + cls.local_salt = { # 'dns.A': dnsutil.A, # 'dns.AAAA': dnsutil.AAAA, # 'file.exists': filemod.file_exists, @@ -704,6 +697,10 @@ def __init__(self, *args, **kws): # 'file.dirname': filemod.dirname } + @classmethod + def tearDownClass(cls): + cls.local_opts = cls.local_salt = None + def test_regex_escape(self): dataset = "foo?:.*/\\bar" env = Environment(extensions=[SerializerExtension]) @@ -716,51 +713,39 @@ def test_unique_string(self): unique = set(dataset) env = Environment(extensions=[SerializerExtension]) env.filters.update(JinjaFilter.salt_jinja_filters) - if six.PY3: - rendered = ( - env.from_string("{{ dataset|unique }}") - .render(dataset=dataset) - .strip("'{}") - .split("', '") - ) - self.assertEqual(sorted(rendered), sorted(list(unique))) - else: - rendered = env.from_string("{{ dataset|unique }}").render(dataset=dataset) - self.assertEqual(rendered, "{0}".format(unique)) + rendered = ( + env.from_string("{{ dataset|unique }}") + .render(dataset=dataset) + .strip("'{}") + .split("', '") + ) + self.assertEqual(sorted(rendered), sorted(list(unique))) def test_unique_tuple(self): dataset = ("foo", "foo", "bar") unique = set(dataset) env = Environment(extensions=[SerializerExtension]) env.filters.update(JinjaFilter.salt_jinja_filters) - if six.PY3: - rendered = ( - env.from_string("{{ dataset|unique }}") - .render(dataset=dataset) - .strip("'{}") - .split("', '") - ) - self.assertEqual(sorted(rendered), sorted(list(unique))) - else: - rendered = env.from_string("{{ dataset|unique }}").render(dataset=dataset) - self.assertEqual(rendered, "{0}".format(unique)) + rendered = ( + env.from_string("{{ dataset|unique }}") + .render(dataset=dataset) + .strip("'{}") + .split("', '") + ) + self.assertEqual(sorted(rendered), sorted(list(unique))) def test_unique_list(self): dataset = ["foo", "foo", "bar"] unique = ["foo", "bar"] env = Environment(extensions=[SerializerExtension]) env.filters.update(JinjaFilter.salt_jinja_filters) - if six.PY3: - rendered = ( - env.from_string("{{ dataset|unique }}") - .render(dataset=dataset) - .strip("'[]") - .split("', '") - ) - self.assertEqual(rendered, unique) - else: - rendered = env.from_string("{{ dataset|unique }}").render(dataset=dataset) - self.assertEqual(rendered, "{0}".format(unique)) + rendered = ( + env.from_string("{{ dataset|unique }}") + .render(dataset=dataset) + .strip("'[]") + .split("', '") + ) + self.assertEqual(rendered, unique) def test_serialize_json(self): dataset = {"foo": True, "bar": 42, "baz": [1, 2, 3], "qux": 2.0} @@ -790,17 +775,7 @@ def test_serialize_yaml_unicode(self): dataset = "str value" env = Environment(extensions=[SerializerExtension]) rendered = env.from_string("{{ dataset|yaml }}").render(dataset=dataset) - if six.PY3: - self.assertEqual("str value", rendered) - else: - # Due to a bug in the equality handler, this check needs to be split - # up into several different assertions. We need to check that the various - # string segments are present in the rendered value, as well as the - # type of the rendered variable (should be unicode, which is the same as - # six.text_type). This should cover all use cases but also allow the test - # to pass on CentOS 6 running Python 2.7. - self.assertIn("str value", rendered) - self.assertIsInstance(rendered, six.text_type) + self.assertEqual("str value", rendered) def test_serialize_python(self): dataset = {"foo": True, "bar": 42, "baz": [1, 2, 3], "qux": 2.0} @@ -971,20 +946,14 @@ def test_nested_structures(self): rendered = env.from_string("{{ data }}").render(data=data) self.assertEqual( - rendered, - "{u'foo': {u'bar': u'baz', u'qux': 42}}" - if six.PY2 - else "{'foo': {'bar': 'baz', 'qux': 42}}", + rendered, "{'foo': {'bar': 'baz', 'qux': 42}}", ) rendered = env.from_string("{{ data }}").render( data=[OrderedDict(foo="bar",), OrderedDict(baz=42,)] ) self.assertEqual( - rendered, - "[{'foo': u'bar'}, {'baz': 42}]" - if six.PY2 - else "[{'foo': 'bar'}, {'baz': 42}]", + rendered, "[{'foo': 'bar'}, {'baz': 42}]", ) def test_set_dict_key_value(self): @@ -1026,10 +995,7 @@ def test_update_dict_key_value(self): ), ) self.assertEqual( - rendered, - "{u'bar': {u'baz': {u'qux': 1, u'quux': 3}}}" - if six.PY2 - else "{'bar': {'baz': {'qux': 1, 'quux': 3}}}", + rendered, "{'bar': {'baz': {'qux': 1, 'quux': 3}}}", ) # Test incorrect usage @@ -1071,10 +1037,7 @@ def test_append_dict_key_value(self): ), ) self.assertEqual( - rendered, - "{u'bar': {u'baz': [1, 2, 42]}}" - if six.PY2 - else "{'bar': {'baz': [1, 2, 42]}}", + rendered, "{'bar': {'baz': [1, 2, 42]}}", ) def test_extend_dict_key_value(self): @@ -1097,10 +1060,7 @@ def test_extend_dict_key_value(self): ), ) self.assertEqual( - rendered, - "{u'bar': {u'baz': [1, 2, 42, 43]}}" - if six.PY2 - else "{'bar': {'baz': [1, 2, 42, 43]}}", + rendered, "{'bar': {'baz': [1, 2, 42, 43]}}", ) # Edge cases rendered = render_jinja_tmpl( @@ -1572,30 +1532,42 @@ def test_symmetric_difference(self): self.assertEqual(rendered, "1, 4") def test_method_call(self): - ''' + """ Test the `method_call` Jinja filter. - ''' - rendered = render_jinja_tmpl("{{ 6|method_call('bit_length') }}", - dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + """ + rendered = render_jinja_tmpl( + "{{ 6|method_call('bit_length') }}", + dict(opts=self.local_opts, saltenv="test", salt=self.local_salt), + ) self.assertEqual(rendered, "3") - rendered = render_jinja_tmpl("{{ 6.7|method_call('is_integer') }}", - dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + rendered = render_jinja_tmpl( + "{{ 6.7|method_call('is_integer') }}", + dict(opts=self.local_opts, saltenv="test", salt=self.local_salt), + ) self.assertEqual(rendered, "False") - rendered = render_jinja_tmpl("{{ 'absaltba'|method_call('strip', 'ab') }}", - dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + rendered = render_jinja_tmpl( + "{{ 'absaltba'|method_call('strip', 'ab') }}", + dict(opts=self.local_opts, saltenv="test", salt=self.local_salt), + ) self.assertEqual(rendered, "salt") - rendered = render_jinja_tmpl("{{ [1, 2, 1, 3, 4]|method_call('index', 1, 1, 3) }}", - dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + rendered = render_jinja_tmpl( + "{{ [1, 2, 1, 3, 4]|method_call('index', 1, 1, 3) }}", + dict(opts=self.local_opts, saltenv="test", salt=self.local_salt), + ) self.assertEqual(rendered, "2") # have to use `dictsort` to keep test result deterministic - rendered = render_jinja_tmpl("{{ {}|method_call('fromkeys', ['a', 'b', 'c'], 0)|dictsort }}", - dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + rendered = render_jinja_tmpl( + "{{ {}|method_call('fromkeys', ['a', 'b', 'c'], 0)|dictsort }}", + dict(opts=self.local_opts, saltenv="test", salt=self.local_salt), + ) self.assertEqual(rendered, "[('a', 0), ('b', 0), ('c', 0)]") # missing object method test - rendered = render_jinja_tmpl("{{ 6|method_call('bit_width') }}", - dict(opts=self.local_opts, saltenv='test', salt=self.local_salt)) + rendered = render_jinja_tmpl( + "{{ 6|method_call('bit_width') }}", + dict(opts=self.local_opts, saltenv="test", salt=self.local_salt), + ) self.assertEqual(rendered, "None") def test_md5(self):