From 250606514268ee93c41d2e3e952a87486f49faf3 Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Sat, 17 May 2025 13:19:09 +0200 Subject: [PATCH] Optimized safe logic --- filter_plugins/safe.py | 55 +++++++++++++++++++ filter_plugins/safe_var.py | 27 ---------- tests/unit/test_safe_placeholders_filter.py | 59 +++++++++++++++++++++ tests/unit/test_safe_var.py | 2 +- 4 files changed, 115 insertions(+), 28 deletions(-) create mode 100644 filter_plugins/safe.py delete mode 100644 filter_plugins/safe_var.py create mode 100644 tests/unit/test_safe_placeholders_filter.py diff --git a/filter_plugins/safe.py b/filter_plugins/safe.py new file mode 100644 index 00000000..f1452d68 --- /dev/null +++ b/filter_plugins/safe.py @@ -0,0 +1,55 @@ +from jinja2 import Undefined + + +def safe_placeholders(template: str, mapping: dict = None) -> str: + """ + Format a template like "{url}/logo.png". + If mapping is provided (not None) and ANY placeholder is missing or maps to None/empty string, the function will raise KeyError. + If mapping is None, missing placeholders or invalid templates return empty string. + Numerical zero or False are considered valid values. + Any other formatting errors return an empty string. + """ + # Non-string templates yield empty + if not isinstance(template, str): + return '' + + class SafeDict(dict): + def __getitem__(self, key): + val = super().get(key, None) + # Treat None or empty string as missing + if val is None or (isinstance(val, str) and val == ''): + raise KeyError(key) + return val + def __missing__(self, key): + raise KeyError(key) + + silent = mapping is None + data = mapping or {} + try: + return template.format_map(SafeDict(data)) + except KeyError: + if silent: + return '' + raise + except Exception: + return '' + +def safe_var(value): + """ + Ansible filter: returns the value unchanged unless it's Undefined or None, + in which case returns an empty string. + Catches all exceptions and yields ''. + """ + try: + if isinstance(value, Undefined) or value is None: + return '' + return value + except Exception: + return '' + +class FilterModule(object): + def filters(self): + return { + 'safe_var': safe_var, + 'safe_placeholders': safe_placeholders, + } diff --git a/filter_plugins/safe_var.py b/filter_plugins/safe_var.py deleted file mode 100644 index df82f323..00000000 --- a/filter_plugins/safe_var.py +++ /dev/null @@ -1,27 +0,0 @@ -# file: filter_plugins/safe_var.py - -from jinja2 import Undefined - -def safe_var(value): - """ - Returns the original value unless it is None or Jinja2‐Undefined. - Catches all exceptions and returns an empty string on error. - """ - try: - # If the value is an Undefined from Jinja2, treat it as missing - if isinstance(value, Undefined): - return '' - # Treat None as missing as well - if value is None: - return '' - # Otherwise return the actual value - return value - except Exception: - # Catch any other errors and return empty string - return '' - -class FilterModule(object): - def filters(self): - return { - 'safe_var': safe_var - } diff --git a/tests/unit/test_safe_placeholders_filter.py b/tests/unit/test_safe_placeholders_filter.py new file mode 100644 index 00000000..4150e485 --- /dev/null +++ b/tests/unit/test_safe_placeholders_filter.py @@ -0,0 +1,59 @@ +import unittest +import sys +import os + +# Ensure filter_plugins directory is on the path +sys.path.insert( + 0, + os.path.abspath(os.path.join(os.path.dirname(__file__), '../../filter_plugins')) +) + +from safe import safe_placeholders + +class TestSafePlaceholdersFilter(unittest.TestCase): + def test_simple_replacement(self): + template = "Hello, {user}!" + mapping = {'user': 'Alice'} + self.assertEqual(safe_placeholders(template, mapping), "Hello, Alice!") + + def test_missing_placeholder(self): + template = "Hello, {user}!" + # Missing placeholder should raise KeyError + with self.assertRaises(KeyError): + safe_placeholders(template, {}) + + def test_none_template(self): + self.assertEqual(safe_placeholders(None, {'user': 'Alice'}), "") + + def test_no_placeholders(self): + template = "Just a plain string" + mapping = {'any': 'value'} + self.assertEqual(safe_placeholders(template, mapping), "Just a plain string") + + def test_multiple_placeholders(self): + template = "{greet}, {user}!" + mapping = {'greet': 'Hi', 'user': 'Bob'} + self.assertEqual(safe_placeholders(template, mapping), "Hi, Bob!") + + def test_numeric_values(self): + template = "Count: {n}" + mapping = {'n': 0} + self.assertEqual(safe_placeholders(template, mapping), "Count: 0") + + def test_extra_mapping_keys(self): + template = "Value: {a}" + mapping = {'a': '1', 'b': '2'} + self.assertEqual(safe_placeholders(template, mapping), "Value: 1") + + def test_malformed_template(self): + # Unclosed placeholder should be caught and return empty string + template = "Unclosed {key" + mapping = {'key': 'value'} + self.assertEqual(safe_placeholders(template, mapping), "") + + def test_mapping_none(self): + template = "Test {x}" + self.assertEqual(safe_placeholders(template, None), "") + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_safe_var.py b/tests/unit/test_safe_var.py index 6811bbcc..8564b19b 100644 --- a/tests/unit/test_safe_var.py +++ b/tests/unit/test_safe_var.py @@ -9,7 +9,7 @@ sys.path.insert( os.path.abspath(os.path.join(os.path.dirname(__file__), '../../filter_plugins')) ) -from safe_var import FilterModule +from safe import FilterModule class TestSafeVarFilter(unittest.TestCase): def setUp(self):