Skip to content

Commit

Permalink
Support enforcing nonempty values in ListOf (#268)
Browse files Browse the repository at this point in the history
This adds empty value validation to ListOf.
  • Loading branch information
willkg committed Oct 30, 2024
1 parent b4622cd commit bd16529
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 17 deletions.
48 changes: 40 additions & 8 deletions src/everett/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@
"ConfigDictEnv",
"ConfigEnvFileEnv",
"ConfigManager",
"ConfigOSEnv",
"ConfigObjEnv",
"ListOf",
"Option",
"ConfigOSEnv",
"config_override",
"get_config_for_class",
"get_runtime_config",
"ListOf",
"Option",
"parse_bool",
"parse_class",
"parse_data_size",
"parse_time_period",
"parse_env_file",
"parse_time_period",
]


Expand Down Expand Up @@ -534,7 +534,7 @@ def get_key_from_envs(envs: Iterable[Any], key: str) -> Union[str, NoValue]:


class ListOf:
"""Parse a comma-separated list of things.
"""Parse a delimiter-separated list of things.
After delimiting items, this strips the whitespace at the beginning and end
of each string. Then it passes each string into the parser to get the final
Expand All @@ -550,6 +550,25 @@ class ListOf:
>>> ListOf(str)('1, 2 ,3,4')
['1', '2', '3', '4']
``ListOf`` defaults to using a comma as a delimiter, but supports other
delimiters:
>>> ListOf(str, delimiter=":")("/path/a/:/path/b/")
['/path/a/', '/path/b/']
``ListOf`` supports raising a configuration error when one of the values
is an empty string:
>>> ListOf(str, allow_empty=False)("a,,b")
Traceback (most recent call last):
...
ValueError: 'a,,b' can not have empty values
The user will get a configuration error like this::
ValueError: 'a,,b' can not have empty values
NAMES requires a value parseable by <ListOf(str, delimiter=',', allow_empty=False)>
Note: This doesn't handle quotes or backslashes or any complicated string
parsing.
Expand All @@ -560,19 +579,32 @@ class ListOf:
"""

def __init__(self, parser: Callable, delimiter: str = ","):
def __init__(
self, parser: Callable, delimiter: str = ",", allow_empty: bool = True
):
self.sub_parser = parser
self.delimiter = delimiter
self.allow_empty = allow_empty

def __call__(self, value: str) -> list[Any]:
parser = get_parser(self.sub_parser)
if value:
return [parser(token.strip()) for token in value.split(self.delimiter)]
parsed_values = []
for token in value.split(self.delimiter):
token = token.strip()
if not token and not self.allow_empty:
raise ValueError(f"{value!r} can not have empty values")
parsed_values.append(parser(token))
return parsed_values
else:
return []

def __repr__(self) -> str:
return f"<ListOf({qualname(self.sub_parser)})>"
return (
f"<ListOf({qualname(self.sub_parser)}, "
+ f"delimiter={self.delimiter!r}, "
+ f"allow_empty={self.allow_empty!r})>"
)


class ChoiceOf:
Expand Down
30 changes: 22 additions & 8 deletions tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,32 @@

from everett import (
ConfigurationError,
InvalidValueError,
ConfigurationMissingError,
InvalidValueError,
NO_VALUE,
)
import everett.manager
from everett.manager import (
ChoiceOf,
ConfigDictEnv,
ConfigEnvFileEnv,
ConfigManager,
ConfigOSEnv,
ConfigObjEnv,
ChoiceOf,
ListOf,
Option,
ConfigOSEnv,
config_override,
generate_uppercase_key,
get_config_for_class,
get_key_from_envs,
get_parser,
get_runtime_config,
listify,
ListOf,
Option,
parse_bool,
parse_class,
parse_data_size,
parse_time_period,
parse_env_file,
parse_time_period,
qualname,
)

Expand All @@ -53,7 +53,7 @@
# class method
(ConfigManager.basic_config, "everett.manager.ConfigManager.basic_config"),
# instance
(ListOf(bool), "<ListOf(bool)>"),
(ListOf(bool), "<ListOf(bool, delimiter=',', allow_empty=True)>"),
# instance
(ChoiceOf(int, ["1", "10", "100"]), "<ChoiceOf(int, ['1', '10', '100'])>"),
# instance method
Expand Down Expand Up @@ -279,6 +279,7 @@ def test_ListOf():
assert ListOf(int)("1,2,3") == [1, 2, 3]
assert ListOf(str)("1 , 2, 3") == ["1", "2", "3"]
assert ListOf(int, delimiter=":")("1:2") == [1, 2]
assert ListOf(str)("a,,b") == ["a", "", "b"]


def test_ListOf_error():
Expand All @@ -288,7 +289,20 @@ def test_ListOf_error():

assert str(exc_info.value) == (
"ValueError: 'badbool' is not a valid bool value\n"
"BOOLS requires a value parseable by <ListOf(bool)>"
"BOOLS requires a value parseable by "
"<ListOf(bool, delimiter=',', allow_empty=True)>"
)


def test_ListOf_allow_empty_error():
config = ConfigManager.from_dict({"names": "bob,,alice"})
with pytest.raises(InvalidValueError) as exc_info:
config("names", parser=ListOf(str, allow_empty=False))

assert str(exc_info.value) == (
"ValueError: 'bob,,alice' can not have empty values\n"
"NAMES requires a value parseable by "
"<ListOf(str, delimiter=',', allow_empty=False)>"
)


Expand Down
2 changes: 1 addition & 1 deletion tests/test_sphinxext.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ def test_option_parser(self, tmpdir, capsys):
user_listof
Parser:
*<ListOf(str)>*
*<ListOf(str, delimiter=',', allow_empty=True)>*
Required:
Yes
Expand Down

0 comments on commit bd16529

Please sign in to comment.