Skip to content

Commit 237ebdf

Browse files
tomgrin10sloria
andauthored
Add enum parser (#185)
* Add enum parser * Add enum description to readme * Add ignore_case argument to enum parser * Changed enum to choice in env.parser_for example in README * Make type arg in enum parser positional * Update changelog Co-authored-by: Steven Loria <sloria1@gmail.com>
1 parent 994e901 commit 237ebdf

4 files changed

Lines changed: 57 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
Features:
66

7+
- Add `enum` parser ([#185](https://github.com/sloria/environs/pull/185)).
78
- Add `delimiter` param to `env.list`
89
([#184](https://github.com/sloria/environs/pull/184)).
9-
Thanks [tomgrin10](https://github.com/tomgrin10?) for the PR.
10+
11+
Thanks [tomgrin10](https://github.com/tomgrin10?) for the PRs.
1012

1113
Bug fixes:
1214

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ The following are all type-casting methods of `Env`:
109109
- `env.uuid`
110110
- `env.log_level`
111111
- `env.path` (casts to a [`pathlib.Path`](https://docs.python.org/3/library/pathlib.html))
112+
- `env.enum` (casts to any given enum type specified in `type` keyword argument, accepts optional `ignore_case` keyword argument)
112113

113114
## Reading `.env` files
114115

@@ -286,14 +287,14 @@ domain = env.furl("DOMAIN") # => furl('https://myapp.com')
286287

287288

288289
# Custom parsers can take extra keyword arguments
289-
@env.parser_for("enum")
290-
def enum_parser(value, choices):
290+
@env.parser_for("choice")
291+
def choice_parser(value, choices):
291292
if value not in choices:
292293
raise environs.EnvError("Invalid!")
293294
return value
294295

295296

296-
color = env.enum("COLOR", choices=["black"]) # => raises EnvError
297+
color = env.choice("COLOR", choices=["black"]) # => raises EnvError
297298
```
298299

299300
## Usage with Flask

environs/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import typing
1010
import types
1111
from collections.abc import Mapping
12+
from enum import Enum
1213
from urllib.parse import urlparse, ParseResult
1314
from pathlib import Path
1415

@@ -177,6 +178,25 @@ def _preprocess_json(value: str, **kwargs):
177178
return pyjson.loads(value)
178179

179180

181+
_EnumT = typing.TypeVar("_EnumT", bound=Enum)
182+
183+
184+
def _enum_parser(value, type: typing.Type[_EnumT], ignore_case: bool = False) -> _EnumT:
185+
invalid_exc = ma.ValidationError(f"Not a valid '{type.__name__}' enum.")
186+
187+
if not ignore_case:
188+
try:
189+
return type[value]
190+
except KeyError as error:
191+
raise invalid_exc from error
192+
193+
for enum_value in type:
194+
if enum_value.name.lower() == value.lower():
195+
return enum_value
196+
197+
raise invalid_exc
198+
199+
180200
def _dj_db_url_parser(value: str, **kwargs) -> dict:
181201
try:
182202
import dj_database_url
@@ -276,6 +296,7 @@ class Env:
276296
timedelta = _field2method(ma.fields.TimeDelta, "timedelta")
277297
uuid = _field2method(ma.fields.UUID, "uuid")
278298
url = _field2method(URLField, "url")
299+
enum = _func2method(_enum_parser, "enum")
279300
dj_db_url = _func2method(_dj_db_url_parser, "dj_db_url")
280301
dj_email_url = _func2method(_dj_email_url_parser, "dj_email_url")
281302
dj_cache_url = _func2method(_dj_cache_url_parser, "dj_cache_url")

tests/test_environs.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import urllib.parse
66
import pathlib
77
from decimal import Decimal
8+
from enum import Enum
89

910
import dj_database_url
1011
import dj_email_url
@@ -35,6 +36,12 @@ class FauxTestException(Exception):
3536
pass
3637

3738

39+
class DayEnum(Enum):
40+
SUNDAY = 1
41+
MONDAY = 2
42+
TUESDAY = 3
43+
44+
3845
class TestCasting:
3946
def test_call(self, set_env, env):
4047
set_env({"STR": "foo", "INT": "42"})
@@ -204,6 +211,24 @@ def test_invalid_url(self, url, set_env, env):
204211
env.url("URL")
205212
assert 'Environment variable "URL" invalid' in excinfo.value.args[0]
206213

214+
def test_enum_cast(self, set_env, env):
215+
set_env({"DAY": "SUNDAY"})
216+
assert env.enum("DAY", type=DayEnum) == DayEnum.SUNDAY
217+
218+
def test_enum_cast_ignore_case(self, set_env, env):
219+
set_env({"DAY": "suNDay"})
220+
assert env.enum("DAY", type=DayEnum, ignore_case=True) == DayEnum.SUNDAY
221+
222+
def test_invalid_enum(self, set_env, env):
223+
set_env({"DAY": "suNDay"})
224+
with pytest.raises(environs.EnvError):
225+
assert env.enum("DAY", type=DayEnum)
226+
227+
def test_invalid_enum_ignore_case(self, set_env, env):
228+
set_env({"DAY": "SonDAY"})
229+
with pytest.raises(environs.EnvError):
230+
assert env.enum("DAY", type=DayEnum, ignore_case=True)
231+
207232

208233
class TestEnvFileReading:
209234
def test_read_env(self, env):
@@ -320,17 +345,17 @@ def https_url(value):
320345
def test_parser_function_can_take_extra_arguments(self, set_env, env):
321346
set_env({"ENV": "dev"})
322347

323-
@env.parser_for("enum")
324-
def enum_parser(value, choices):
348+
@env.parser_for("choice")
349+
def choice_parser(value, choices):
325350
if value not in choices:
326351
raise environs.EnvError("Invalid!")
327352
return value
328353

329-
assert env.enum("ENV", choices=["dev", "prod"]) == "dev"
354+
assert env.choice("ENV", choices=["dev", "prod"]) == "dev"
330355

331356
set_env({"ENV": "invalid"})
332357
with pytest.raises(environs.EnvError):
333-
env.enum("ENV", choices=["dev", "prod"])
358+
env.choice("ENV", choices=["dev", "prod"])
334359

335360
def test_add_parser_from_field(self, set_env, env):
336361
class HTTPSURL(fields.Field):

0 commit comments

Comments
 (0)