Skip to content

Commit

Permalink
feat(misc): primitive parsing from environment variables
Browse files Browse the repository at this point in the history
  • Loading branch information
wpbonelli committed Jan 21, 2024
1 parent fd21500 commit e15ed3b
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 33 deletions.
59 changes: 59 additions & 0 deletions autotest/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
get_packages,
has_package,
has_pkg,
parse_bool,
parse_float,
parse_int,
set_dir,
set_env,
timed,
Expand Down Expand Up @@ -280,3 +283,59 @@ def sleep1dec():
cap = capfd.readouterr()
print(cap.out)
assert re.match(r"sleep1dec took \d+\.\d+ ms", cap.out)


def test_parse_bool_default():
assert parse_bool("TEST_VALUE", default=True)


def test_parse_int_default():
assert parse_int("TEST_VALUE", default=2) == 2


def test_parse_float_default():
assert parse_float("TEST_VALUE", default=1.1) == 1.1


@pytest.mark.parametrize(
"value, expected",
[
("true", True),
("True", True),
("TRUE", True),
("false", False),
("False", False),
("FALSE", False),
("", None),
("garbage", None),
(1, None),
(1.0, None),
],
)
def test_parse_bool(value, expected):
with set_env(TEST_VALUE=str(value)):
assert parse_bool("TEST_VALUE") == expected


@pytest.mark.parametrize(
"value, expected",
[("1", 1), ("1.", 1), ("1.0", 1), ("1.9", 1), ("", None), ("true", None)],
)
def test_parse_int(value, expected):
with set_env(TEST_VALUE=str(value)):
assert parse_int("TEST_VALUE") == expected


@pytest.mark.parametrize(
"value, expected",
[
("1", 1.0),
("", None),
("1.", 1.0),
("1.0", 1.0),
("1.11", 1.11),
],
)
def test_parse_float(value, expected):
with set_env(TEST_VALUE=str(value)):
assert parse_float("TEST_VALUE") == expected
103 changes: 70 additions & 33 deletions modflow_devtools/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import socket
import sys
import traceback
from ast import literal_eval
from contextlib import contextmanager
from functools import wraps
from importlib import metadata
Expand Down Expand Up @@ -31,39 +32,6 @@ def set_dir(path: PathLike):
print(f"Returned to previous directory: {origin}")


@contextmanager
def set_env(*remove, **update):
"""
Temporarily updates the ``os.environ`` dictionary in-place.
Referenced from https://stackoverflow.com/a/34333710/6514033.
The ``os.environ`` dictionary is updated in-place so that the modification
is sure to work in all situations.
:param remove: Environment variables to remove.
:param update: Dictionary of environment variables and values to add/update.
"""
env = environ
update = update or {}
remove = remove or []

# List of environment variables being updated or removed.
stomped = (set(update.keys()) | set(remove)) & set(env.keys())
# Environment variables and values to restore on exit.
update_after = {k: env[k] for k in stomped}
# Environment variables and values to remove on exit.
remove_after = frozenset(k for k in update if k not in env)

try:
env.update(update)
[env.pop(k, None) for k in remove]
yield
finally:
env.update(update_after)
[env.pop(k) for k in remove_after]


class add_sys_path:
"""
Context manager to add temporarily to the system path.
Expand Down Expand Up @@ -486,3 +454,72 @@ def call():
return res

return _timed


def parse_bool(name: str, default: Optional[bool] = None) -> Optional[bool]:
"""Try to parse a boolean from the environment variable with the given name,
return None or an optional default value if it doesn't exist or parsing fails."""
try:
v = literal_eval(environ.get(name).lower().title())
except:
return default
if isinstance(v, bool):
return v
return None


def parse_int(name: str, default: Optional[int] = None) -> Optional[int]:
"""Try to parse an integer from the environment variable with the given name,
return None or an optional default value if it doesn't exist or parsing fails."""
try:
v = literal_eval(environ.get(name))
except:
return default
if isinstance(v, (int, float)):
return int(v)
return None


def parse_float(name: str, default: Optional[float] = None) -> Optional[float]:
"""Try to parse a float from the environment variable with the given name,
return None or an optional default value if it doesn't exist or parsing fails."""
try:
v = literal_eval(environ.get(name))
except:
return default
if isinstance(v, (int, float)):
return float(v)
return None


@contextmanager
def set_env(*remove, **update):
"""
Temporarily updates the ``os.environ`` dictionary in-place.
Referenced from https://stackoverflow.com/a/34333710/6514033.
The ``os.environ`` dictionary is updated in-place so that the modification
is sure to work in all situations.
:param remove: Environment variables to remove.
:param update: Dictionary of environment variables and values to add/update.
"""
env = environ
update = update or {}
remove = remove or []

# List of environment variables being updated or removed.
stomped = (set(update.keys()) | set(remove)) & set(env.keys())
# Environment variables and values to restore on exit.
update_after = {k: env[k] for k in stomped}
# Environment variables and values to remove on exit.
remove_after = frozenset(k for k in update if k not in env)

try:
env.update(update)
[env.pop(k, None) for k in remove]
yield
finally:
env.update(update_after)
[env.pop(k) for k in remove_after]

0 comments on commit e15ed3b

Please sign in to comment.