From 4885331509eeffe50f42d76b234996467b06170f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 13 May 2022 18:46:49 +0200 Subject: [PATCH] Fail template functions when no default specified (#71687) --- homeassistant/helpers/template.py | 90 +++---- .../fixtures/broken_configuration.yaml | 4 +- .../fixtures/sensor_configuration.yaml | 4 +- tests/helpers/test_event.py | 2 +- tests/helpers/test_template.py | 226 ++++++++++++++---- 5 files changed, 213 insertions(+), 113 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3f084674a1b..d1ed9d06db9 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1325,21 +1325,12 @@ def utcnow(hass: HomeAssistant) -> datetime: return dt_util.utcnow() -def warn_no_default(function, value, default): +def raise_no_default(function, value): """Log warning if no default is specified.""" template, action = template_cv.get() or ("", "rendering or compiling") - _LOGGER.warning( - ( - "Template warning: '%s' got invalid input '%s' when %s template '%s' " - "but no default was specified. Currently '%s' will return '%s', however this template will fail " - "to render in Home Assistant core 2022.6" - ), - function, - value, - action, - template, - function, - default, + raise ValueError( + f"Template error: {function} got invalid input '{value}' when {action} template '{template}' " + "but no default was specified" ) @@ -1361,8 +1352,7 @@ def forgiving_round(value, precision=0, method="common", default=_SENTINEL): except (ValueError, TypeError): # If value can't be converted to float if default is _SENTINEL: - warn_no_default("round", value, value) - return value + raise_no_default("round", value) return default @@ -1373,20 +1363,25 @@ def multiply(value, amount, default=_SENTINEL): except (ValueError, TypeError): # If value can't be converted to float if default is _SENTINEL: - warn_no_default("multiply", value, value) - return value + raise_no_default("multiply", value) return default def logarithm(value, base=math.e, default=_SENTINEL): """Filter and function to get logarithm of the value with a specific base.""" try: - return math.log(float(value), float(base)) + value_float = float(value) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("log", value, value) - return value + raise_no_default("log", value) return default + try: + base_float = float(base) + except (ValueError, TypeError): + if default is _SENTINEL: + raise_no_default("log", base) + return default + return math.log(value_float, base_float) def sine(value, default=_SENTINEL): @@ -1395,8 +1390,7 @@ def sine(value, default=_SENTINEL): return math.sin(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("sin", value, value) - return value + raise_no_default("sin", value) return default @@ -1406,8 +1400,7 @@ def cosine(value, default=_SENTINEL): return math.cos(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("cos", value, value) - return value + raise_no_default("cos", value) return default @@ -1417,8 +1410,7 @@ def tangent(value, default=_SENTINEL): return math.tan(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("tan", value, value) - return value + raise_no_default("tan", value) return default @@ -1428,8 +1420,7 @@ def arc_sine(value, default=_SENTINEL): return math.asin(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("asin", value, value) - return value + raise_no_default("asin", value) return default @@ -1439,8 +1430,7 @@ def arc_cosine(value, default=_SENTINEL): return math.acos(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("acos", value, value) - return value + raise_no_default("acos", value) return default @@ -1450,8 +1440,7 @@ def arc_tangent(value, default=_SENTINEL): return math.atan(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("atan", value, value) - return value + raise_no_default("atan", value) return default @@ -1474,8 +1463,7 @@ def arc_tangent2(*args, default=_SENTINEL): return math.atan2(float(args[0]), float(args[1])) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("atan2", args, args) - return args + raise_no_default("atan2", args) return default @@ -1485,8 +1473,7 @@ def square_root(value, default=_SENTINEL): return math.sqrt(float(value)) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("sqrt", value, value) - return value + raise_no_default("sqrt", value) return default @@ -1502,8 +1489,7 @@ def timestamp_custom(value, date_format=DATE_STR_FORMAT, local=True, default=_SE except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: - warn_no_default("timestamp_custom", value, value) - return value + raise_no_default("timestamp_custom", value) return default @@ -1514,8 +1500,7 @@ def timestamp_local(value, default=_SENTINEL): except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: - warn_no_default("timestamp_local", value, value) - return value + raise_no_default("timestamp_local", value) return default @@ -1526,8 +1511,7 @@ def timestamp_utc(value, default=_SENTINEL): except (ValueError, TypeError): # If timestamp can't be converted if default is _SENTINEL: - warn_no_default("timestamp_utc", value, value) - return value + raise_no_default("timestamp_utc", value) return default @@ -1537,8 +1521,7 @@ def forgiving_as_timestamp(value, default=_SENTINEL): return dt_util.as_timestamp(value) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("as_timestamp", value, None) - return None + raise_no_default("as_timestamp", value) return default @@ -1558,8 +1541,7 @@ def strptime(string, fmt, default=_SENTINEL): return datetime.strptime(string, fmt) except (ValueError, AttributeError, TypeError): if default is _SENTINEL: - warn_no_default("strptime", string, string) - return string + raise_no_default("strptime", string) return default @@ -1618,8 +1600,7 @@ def forgiving_float(value, default=_SENTINEL): return float(value) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("float", value, value) - return value + raise_no_default("float", value) return default @@ -1629,26 +1610,23 @@ def forgiving_float_filter(value, default=_SENTINEL): return float(value) except (ValueError, TypeError): if default is _SENTINEL: - warn_no_default("float", value, 0) - return 0 + raise_no_default("float", value) return default def forgiving_int(value, default=_SENTINEL, base=10): - """Try to convert value to an int, and warn if it fails.""" + """Try to convert value to an int, and raise if it fails.""" result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: - warn_no_default("int", value, value) - return value + raise_no_default("int", value) return result def forgiving_int_filter(value, default=_SENTINEL, base=10): - """Try to convert value to an int, and warn if it fails.""" + """Try to convert value to an int, and raise if it fails.""" result = jinja2.filters.do_int(value, default=default, base=base) if result is _SENTINEL: - warn_no_default("int", value, 0) - return 0 + raise_no_default("int", value) return result diff --git a/tests/components/template/fixtures/broken_configuration.yaml b/tests/components/template/fixtures/broken_configuration.yaml index de0c8aebd9d..00f773e2fd3 100644 --- a/tests/components/template/fixtures/broken_configuration.yaml +++ b/tests/components/template/fixtures/broken_configuration.yaml @@ -7,8 +7,8 @@ sensor: friendly_name: Combined Sense Energy Usage unit_of_measurement: kW value_template: - "{{ ((states('sensor.energy_usage') | float) + (states('sensor.energy_usage_2') - | float)) / 1000 }}" + "{{ ((states('sensor.energy_usage') | float(default=0)) + (states('sensor.energy_usage_2') + | float(default=0))) / 1000 }}" watching_tv_in_master_bedroom: friendly_name: Watching TV in Master Bedroom value_template: diff --git a/tests/components/template/fixtures/sensor_configuration.yaml b/tests/components/template/fixtures/sensor_configuration.yaml index 02c8cc373f4..f1afef26b87 100644 --- a/tests/components/template/fixtures/sensor_configuration.yaml +++ b/tests/components/template/fixtures/sensor_configuration.yaml @@ -14,8 +14,8 @@ sensor: friendly_name: Combined Sense Energy Usage unit_of_measurement: kW value_template: - "{{ ((states('sensor.energy_usage') | float) + (states('sensor.energy_usage_2') - | float)) / 1000 }}" + "{{ ((states('sensor.energy_usage') | float(default=0)) + (states('sensor.energy_usage_2') + | float(default=0))) / 1000 }}" watching_tv_in_master_bedroom: friendly_name: Watching TV in Master Bedroom value_template: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 9b0a3e1abd5..d0355bba5a8 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1026,7 +1026,7 @@ async def test_track_template_result_none(hass): template_condition = Template("{{state_attr('sensor.test', 'battery')}}", hass) template_condition_var = Template( - "{{(state_attr('sensor.test', 'battery')|int) + test }}", hass + "{{(state_attr('sensor.test', 'battery')|int(default=0)) + test }}", hass ) def specific_run_callback(event, updates): diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d3a63460ec1..b2448bc1b5c 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -233,11 +233,11 @@ def test_float_function(hass): is True ) - assert ( + # Test handling of invalid input + with pytest.raises(TemplateError): template.Template("{{ float('forgiving') }}", hass).async_render() - == "forgiving" - ) + # Test handling of default return value assert render(hass, "{{ float('bad', 1) }}") == 1 assert render(hass, "{{ float('bad', default=1) }}") == 1 @@ -248,7 +248,12 @@ def test_float_filter(hass): assert render(hass, "{{ states.sensor.temperature.state | float }}") == 12.0 assert render(hass, "{{ states.sensor.temperature.state | float > 11 }}") is True - assert render(hass, "{{ 'bad' | float }}") == 0 + + # Test handling of invalid input + with pytest.raises(TemplateError): + render(hass, "{{ 'bad' | float }}") + + # Test handling of default return value assert render(hass, "{{ 'bad' | float(1) }}") == 1 assert render(hass, "{{ 'bad' | float(default=1) }}") == 1 @@ -262,7 +267,11 @@ def test_int_filter(hass): hass.states.async_set("sensor.temperature", "0x10") assert render(hass, "{{ states.sensor.temperature.state | int(base=16) }}") == 16 - assert render(hass, "{{ 'bad' | int }}") == 0 + # Test handling of invalid input + with pytest.raises(TemplateError): + render(hass, "{{ 'bad' | int }}") + + # Test handling of default return value assert render(hass, "{{ 'bad' | int(1) }}") == 1 assert render(hass, "{{ 'bad' | int(default=1) }}") == 1 @@ -276,7 +285,11 @@ def test_int_function(hass): hass.states.async_set("sensor.temperature", "0x10") assert render(hass, "{{ int(states.sensor.temperature.state, base=16) }}") == 16 - assert render(hass, "{{ int('bad') }}") == "bad" + # Test handling of invalid input + with pytest.raises(TemplateError): + render(hass, "{{ int('bad') }}") + + # Test handling of default return value assert render(hass, "{{ int('bad', 1) }}") == 1 assert render(hass, "{{ int('bad', default=1) }}") == 1 @@ -364,12 +377,12 @@ def test_rounding_value(hass): def test_rounding_value_on_error(hass): """Test rounding value handling of error.""" - assert template.Template("{{ None | round }}", hass).async_render() is None + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ None | round }}", hass).async_render() - assert ( + with pytest.raises(TemplateError): template.Template('{{ "no_number" | round }}', hass).async_render() - == "no_number" - ) # Test handling of default return value assert render(hass, "{{ 'no_number' | round(default=1) }}") == 1 @@ -377,7 +390,7 @@ def test_rounding_value_on_error(hass): def test_multiply(hass): """Test multiply.""" - tests = {None: None, 10: 100, '"abcd"': "abcd"} + tests = {10: 100} for inp, out in tests.items(): assert ( @@ -387,6 +400,10 @@ def test_multiply(hass): == out ) + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ abcd | multiply(10) }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | multiply(10, 1) }}") == 1 assert render(hass, "{{ 'no_number' | multiply(10, default=1) }}") == 1 @@ -397,9 +414,7 @@ def test_logarithm(hass): tests = [ (4, 2, 2.0), (1000, 10, 3.0), - (math.e, "", 1.0), - ('"invalid"', "_", "invalid"), - (10, '"invalid"', 10.0), + (math.e, "", 1.0), # The "" means the default base (e) will be used ] for value, base, expected in tests: @@ -417,6 +432,16 @@ def test_logarithm(hass): == expected ) + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ invalid | log(_) }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ log(invalid, _) }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ 10 | log(invalid) }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ log(10, invalid) }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | log(10, 1) }}") == 1 assert render(hass, "{{ 'no_number' | log(10, default=1) }}") == 1 @@ -432,7 +457,6 @@ def test_sine(hass): (math.pi, 0.0), (math.pi * 1.5, -1.0), (math.pi / 10, 0.309), - ('"duck"', "duck"), ] for value, expected in tests: @@ -442,6 +466,12 @@ def test_sine(hass): ) assert render(hass, f"{{{{ sin({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'duck' | sin }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | sin('duck') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | sin(1) }}") == 1 assert render(hass, "{{ 'no_number' | sin(default=1) }}") == 1 @@ -457,7 +487,6 @@ def test_cos(hass): (math.pi, -1.0), (math.pi * 1.5, -0.0), (math.pi / 10, 0.951), - ("'error'", "error"), ] for value, expected in tests: @@ -467,11 +496,17 @@ def test_cos(hass): ) assert render(hass, f"{{{{ cos({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'error' | cos }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | cos('error') }}", hass).async_render() + # Test handling of default return value - assert render(hass, "{{ 'no_number' | sin(1) }}") == 1 - assert render(hass, "{{ 'no_number' | sin(default=1) }}") == 1 - assert render(hass, "{{ sin('no_number', 1) }}") == 1 - assert render(hass, "{{ sin('no_number', default=1) }}") == 1 + assert render(hass, "{{ 'no_number' | cos(1) }}") == 1 + assert render(hass, "{{ 'no_number' | cos(default=1) }}") == 1 + assert render(hass, "{{ cos('no_number', 1) }}") == 1 + assert render(hass, "{{ cos('no_number', default=1) }}") == 1 def test_tan(hass): @@ -482,7 +517,6 @@ def test_tan(hass): (math.pi / 180 * 45, 1.0), (math.pi / 180 * 90, "1.633123935319537e+16"), (math.pi / 180 * 135, -1.0), - ("'error'", "error"), ] for value, expected in tests: @@ -492,6 +526,12 @@ def test_tan(hass): ) assert render(hass, f"{{{{ tan({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'error' | tan }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | tan('error') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | tan(1) }}") == 1 assert render(hass, "{{ 'no_number' | tan(default=1) }}") == 1 @@ -507,7 +547,6 @@ def test_sqrt(hass): (2, 1.414), (10, 3.162), (100, 10.0), - ("'error'", "error"), ] for value, expected in tests: @@ -517,6 +556,12 @@ def test_sqrt(hass): ) assert render(hass, f"{{{{ sqrt({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'error' | sqrt }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | sqrt('error') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | sqrt(1) }}") == 1 assert render(hass, "{{ 'no_number' | sqrt(default=1) }}") == 1 @@ -527,14 +572,11 @@ def test_sqrt(hass): def test_arc_sine(hass): """Test arcus sine.""" tests = [ - (-2.0, -2.0), # value error (-1.0, -1.571), (-0.5, -0.524), (0.0, 0.0), (0.5, 0.524), (1.0, 1.571), - (2.0, 2.0), # value error - ('"error"', "error"), ] for value, expected in tests: @@ -544,6 +586,19 @@ def test_arc_sine(hass): ) assert render(hass, f"{{{{ asin({value}) | round(3) }}}}") == expected + # Test handling of invalid input + invalid_tests = [ + -2.0, # value error + 2.0, # value error + '"error"', + ] + + for value in invalid_tests: + with pytest.raises(TemplateError): + template.Template("{{ %s | asin | round(3) }}" % value, hass).async_render() + with pytest.raises(TemplateError): + assert render(hass, f"{{{{ asin({value}) | round(3) }}}}") + # Test handling of default return value assert render(hass, "{{ 'no_number' | asin(1) }}") == 1 assert render(hass, "{{ 'no_number' | asin(default=1) }}") == 1 @@ -554,14 +609,11 @@ def test_arc_sine(hass): def test_arc_cos(hass): """Test arcus cosine.""" tests = [ - (-2.0, -2.0), # value error (-1.0, 3.142), (-0.5, 2.094), (0.0, 1.571), (0.5, 1.047), (1.0, 0.0), - (2.0, 2.0), # value error - ('"error"', "error"), ] for value, expected in tests: @@ -571,6 +623,19 @@ def test_arc_cos(hass): ) assert render(hass, f"{{{{ acos({value}) | round(3) }}}}") == expected + # Test handling of invalid input + invalid_tests = [ + -2.0, # value error + 2.0, # value error + '"error"', + ] + + for value in invalid_tests: + with pytest.raises(TemplateError): + template.Template("{{ %s | acos | round(3) }}" % value, hass).async_render() + with pytest.raises(TemplateError): + assert render(hass, f"{{{{ acos({value}) | round(3) }}}}") + # Test handling of default return value assert render(hass, "{{ 'no_number' | acos(1) }}") == 1 assert render(hass, "{{ 'no_number' | acos(default=1) }}") == 1 @@ -590,7 +655,6 @@ def test_arc_tan(hass): (1.0, 0.785), (2.0, 1.107), (10.0, 1.471), - ('"error"', "error"), ] for value, expected in tests: @@ -600,6 +664,12 @@ def test_arc_tan(hass): ) assert render(hass, f"{{{{ atan({value}) | round(3) }}}}") == expected + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ 'error' | atan }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ invalid | atan('error') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ 'no_number' | atan(1) }}") == 1 assert render(hass, "{{ 'no_number' | atan(default=1) }}") == 1 @@ -622,7 +692,6 @@ def test_arc_tan2(hass): (-4.0, 3.0, -0.927), (-1.0, 2.0, -0.464), (2.0, 1.0, 1.107), - ('"duck"', '"goose"', ("duck", "goose")), ] for y, x, expected in tests: @@ -639,6 +708,12 @@ def test_arc_tan2(hass): == expected ) + # Test handling of invalid input + with pytest.raises(TemplateError): + template.Template("{{ ('duck', 'goose') | atan2 }}", hass).async_render() + with pytest.raises(TemplateError): + template.Template("{{ atan2('duck', 'goose') }}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ ('duck', 'goose') | atan2(1) }}") == 1 assert render(hass, "{{ ('duck', 'goose') | atan2(default=1) }}") == 1 @@ -655,8 +730,6 @@ def test_strptime(hass): ("2016-10-19", "%Y-%m-%d", None), ("2016", "%Y", None), ("15:22:05", "%H:%M:%S", None), - ("1469119144", "%Y", 1469119144), - ("invalid", "%Y", "invalid"), ] for inp, fmt, expected in tests: @@ -667,6 +740,18 @@ def test_strptime(hass): assert template.Template(temp, hass).async_render() == expected + # Test handling of invalid input + invalid_tests = [ + ("1469119144", "%Y"), + ("invalid", "%Y"), + ] + + for inp, fmt in invalid_tests: + temp = f"{{{{ strptime('{inp}', '{fmt}') }}}}" + + with pytest.raises(TemplateError): + template.Template(temp, hass).async_render() + # Test handling of default return value assert render(hass, "{{ strptime('invalid', '%Y', 1) }}") == 1 assert render(hass, "{{ strptime('invalid', '%Y', default=1) }}") == 1 @@ -677,7 +762,6 @@ def test_timestamp_custom(hass): hass.config.set_time_zone("UTC") now = dt_util.utcnow() tests = [ - (None, None, None, None), (1469119144, None, True, "2016-07-21 16:39:04"), (1469119144, "%Y", True, 2016), (1469119144, "invalid", True, "invalid"), @@ -694,6 +778,22 @@ def test_timestamp_custom(hass): assert template.Template(f"{{{{ {inp} | {fil} }}}}", hass).async_render() == out + # Test handling of invalid input + invalid_tests = [ + (None, None, None), + ] + + for inp, fmt, local in invalid_tests: + if fmt: + fil = f"timestamp_custom('{fmt}')" + elif fmt and local: + fil = f"timestamp_custom('{fmt}', {local})" + else: + fil = "timestamp_custom" + + with pytest.raises(TemplateError): + template.Template(f"{{{{ {inp} | {fil} }}}}", hass).async_render() + # Test handling of default return value assert render(hass, "{{ None | timestamp_custom('invalid', True, 1) }}") == 1 assert render(hass, "{{ None | timestamp_custom(default=1) }}") == 1 @@ -702,14 +802,25 @@ def test_timestamp_custom(hass): def test_timestamp_local(hass): """Test the timestamps to local filter.""" hass.config.set_time_zone("UTC") - tests = {None: None, 1469119144: "2016-07-21T16:39:04+00:00"} + tests = [ + (1469119144, "2016-07-21T16:39:04+00:00"), + ] - for inp, out in tests.items(): + for inp, out in tests: assert ( template.Template("{{ %s | timestamp_local }}" % inp, hass).async_render() == out ) + # Test handling of invalid input + invalid_tests = [ + None, + ] + + for inp in invalid_tests: + with pytest.raises(TemplateError): + template.Template("{{ %s | timestamp_local }}" % inp, hass).async_render() + # Test handling of default return value assert render(hass, "{{ None | timestamp_local(1) }}") == 1 assert render(hass, "{{ None | timestamp_local(default=1) }}") == 1 @@ -1002,18 +1113,26 @@ def test_ordinal(hass): def test_timestamp_utc(hass): """Test the timestamps to local filter.""" now = dt_util.utcnow() - tests = { - None: None, - 1469119144: "2016-07-21T16:39:04+00:00", - dt_util.as_timestamp(now): now.isoformat(), - } + tests = [ + (1469119144, "2016-07-21T16:39:04+00:00"), + (dt_util.as_timestamp(now), now.isoformat()), + ] - for inp, out in tests.items(): + for inp, out in tests: assert ( template.Template("{{ %s | timestamp_utc }}" % inp, hass).async_render() == out ) + # Test handling of invalid input + invalid_tests = [ + None, + ] + + for inp in invalid_tests: + with pytest.raises(TemplateError): + template.Template("{{ %s | timestamp_utc }}" % inp, hass).async_render() + # Test handling of default return value assert render(hass, "{{ None | timestamp_utc(1) }}") == 1 assert render(hass, "{{ None | timestamp_utc(default=1) }}") == 1 @@ -1021,14 +1140,12 @@ def test_timestamp_utc(hass): def test_as_timestamp(hass): """Test the as_timestamp function.""" - assert ( - template.Template('{{ as_timestamp("invalid") }}', hass).async_render() is None - ) - hass.mock = None - assert ( - template.Template("{{ as_timestamp(states.mock) }}", hass).async_render() - is None - ) + with pytest.raises(TemplateError): + template.Template('{{ as_timestamp("invalid") }}', hass).async_render() + + hass.states.async_set("test.object", None) + with pytest.raises(TemplateError): + template.Template("{{ as_timestamp(states.test.object) }}", hass).async_render() tpl = ( '{{ as_timestamp(strptime("2024-02-03T09:10:24+0000", ' @@ -1822,7 +1939,8 @@ def test_distance_function_return_none_if_invalid_state(hass): """Test distance function return None if invalid state.""" hass.states.async_set("test.object_2", "happy", {"latitude": 10}) tpl = template.Template("{{ distance(states.test.object_2) | round }}", hass) - assert tpl.async_render() is None + with pytest.raises(TemplateError): + tpl.async_render() def test_distance_function_return_none_if_invalid_coord(hass): @@ -3359,7 +3477,11 @@ async def test_protected_blocked(hass): async def test_demo_template(hass): """Test the demo template works as expected.""" - hass.states.async_set("sun.sun", "above", {"elevation": 50, "next_rising": "later"}) + hass.states.async_set( + "sun.sun", + "above", + {"elevation": 50, "next_rising": "2022-05-12T03:00:08.503651+00:00"}, + ) for i in range(2): hass.states.async_set(f"sensor.sensor{i}", "on") @@ -3375,7 +3497,7 @@ The temperature is {{ my_test_json.temperature }} {{ my_test_json.unit }}. {% if is_state("sun.sun", "above_horizon") -%} The sun rose {{ relative_time(states.sun.sun.last_changed) }} ago. {%- else -%} - The sun will rise at {{ as_timestamp(strptime(state_attr("sun.sun", "next_rising"), "")) | timestamp_local }}. + The sun will rise at {{ as_timestamp(state_attr("sun.sun", "next_rising")) | timestamp_local }}. {%- endif %} For loop example getting 3 entity values: