diff --git a/notifiers/core.py b/notifiers/core.py index 1a640341..ed2e6623 100644 --- a/notifiers/core.py +++ b/notifiers/core.py @@ -11,7 +11,7 @@ from jsonschema.exceptions import best_match from .exceptions import BadArguments, NoSuchNotifierError, NotificationError, SchemaError -from .utils.helpers import dict_from_environs, merge_dicts +from .utils.helpers import dict_from_environs, merge_dicts, text_to_bool from .utils.schema.formats import format_checker DEFAULT_ENVIRON_PREFIX = "NOTIFIERS_" @@ -162,7 +162,37 @@ def _get_environs(self, prefix: str | None = None) -> dict: if not prefix: log.debug("using default environ prefix") prefix = DEFAULT_ENVIRON_PREFIX - return dict_from_environs(prefix, self.name, list(self.arguments.keys())) + environs = dict_from_environs(prefix, self.name, list(self.arguments.keys())) + return self._coerce_environs(environs) + + def _coerce_environs(self, environs: dict) -> dict: + """ + Coerces environment variable strings to the types declared in the provider schema. + Environment variables are always strings; this converts ``"true"``/``"1"`` to :class:`bool` + and numeric strings to :class:`int` or :class:`float` where the schema requires it. + + :param environs: Raw environ dict (all values are strings) + :return: Environ dict with values cast to schema-declared types + """ + properties = self.arguments + coerced = {} + for key, value in environs.items(): + prop_type = properties.get(key, {}).get("type") + if prop_type == "boolean": + coerced[key] = text_to_bool(value) + elif prop_type == "integer": + try: + coerced[key] = int(value) + except (ValueError, TypeError): + coerced[key] = value + elif prop_type == "number": + try: + coerced[key] = float(value) + except (ValueError, TypeError): + coerced[key] = value + else: + coerced[key] = value + return coerced def _prepare_data(self, data: dict) -> dict: """ diff --git a/tests/test_core.py b/tests/test_core.py index 6fe0fdae..4a27828f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4,7 +4,7 @@ import pytest import notifiers -from notifiers import notify +from notifiers import get_notifier, notify from notifiers.core import SUCCESS_STATUS, Provider, Response from notifiers.exceptions import ( BadArguments, @@ -83,8 +83,6 @@ def test_prepare_data(self, mock_provider): def test_get_notifier(self, mock_provider): """Test ``get_notifier()`` helper function""" - from notifiers import get_notifier - p = get_notifier("mock_provider") assert p assert isinstance(p, Provider) @@ -189,3 +187,19 @@ def test_direct_notify_positive(self, mock_provider): def test_direct_notify_negative(self): with pytest.raises(NoSuchNotifierError, match="No such notifier with name"): notify("foo", message="whateverz") + + def test_environ_bool_and_int_coercion(self, monkeypatch): + """Env vars for boolean/integer schema fields must be coerced from string (issue #387).""" + p = get_notifier("email") + + prefix = "COERCE_TEST_" + env_prefix = prefix + p.name + "_" + monkeypatch.setenv((env_prefix + "tls").upper(), "true") + monkeypatch.setenv((env_prefix + "ssl").upper(), "false") + monkeypatch.setenv((env_prefix + "port").upper(), "587") + + environs = p._get_environs(prefix) + assert environs["tls"] is True, "'true' string must coerce to boolean True" + assert environs["ssl"] is False, "'false' string must coerce to boolean False" + assert environs["port"] == 587, "'587' string must coerce to integer 587" + assert isinstance(environs["port"], int)