Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions notifiers/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_"
Expand Down Expand Up @@ -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:
"""
Expand Down
20 changes: 17 additions & 3 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)