Optimized safe logic

This commit is contained in:
Kevin Veen-Birkenbach 2025-05-17 13:19:09 +02:00
parent cc399e3899
commit 2506065142
No known key found for this signature in database
GPG Key ID: 44D8F11FD62F878E
4 changed files with 115 additions and 28 deletions

55
filter_plugins/safe.py Normal file
View File

@ -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,
}

View File

@ -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 Jinja2Undefined.
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
}

View File

@ -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()

View File

@ -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):