Skip to content

Commit 440ff28

Browse files
authored
Better validation of environment variables (Textualize#5192)
* Better validation of environment variables * wording
1 parent 2277030 commit 440ff28

File tree

2 files changed

+68
-8
lines changed

2 files changed

+68
-8
lines changed

src/textual/constants.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,53 @@ def _get_environ_bool(name: str) -> bool:
2727
return has_environ
2828

2929

30-
def _get_environ_int(name: str, default: int) -> int:
30+
def _get_environ_int(name: str, default: int, minimum: int | None = None) -> int:
3131
"""Retrieves an integer environment variable.
3232
3333
Args:
3434
name: Name of environment variable.
3535
default: The value to use if the value is not set, or set to something other
3636
than a valid integer.
37+
minimum: Optional minimum value.
3738
3839
Returns:
3940
The integer associated with the environment variable if it's set to a valid int
4041
or the default value otherwise.
4142
"""
4243
try:
43-
return int(os.environ[name])
44+
value = int(os.environ[name])
4445
except KeyError:
4546
return default
4647
except ValueError:
4748
return default
49+
if minimum is not None:
50+
return max(minimum, value)
51+
return value
52+
53+
54+
def _get_environ_port(name: str, default: int) -> int:
55+
"""Get a port no. from an environment variable.
56+
57+
Note that there is no 'minimum' here, as ports are more like names than a scalar value.
58+
59+
Args:
60+
name: Name of environment variable.
61+
default: The value to use if the value is not set, or set to something other
62+
than a valid port.
63+
64+
Returns:
65+
An integer port number.
66+
67+
"""
68+
try:
69+
value = int(os.environ[name])
70+
except KeyError:
71+
return default
72+
except ValueError:
73+
return default
74+
if value < 0 or value > 65535:
75+
return default
76+
return value
4877

4978

5079
def _is_valid_animation_level(value: str) -> TypeGuard[AnimationLevel]:
@@ -89,11 +118,11 @@ def _get_textual_animations() -> AnimationLevel:
89118
DEVTOOLS_HOST: Final[str] = get_environ("TEXTUAL_DEVTOOLS_HOST", "127.0.0.1")
90119
"""The host where textual console is running."""
91120

92-
DEVTOOLS_PORT: Final[int] = _get_environ_int("TEXTUAL_DEVTOOLS_PORT", 8081)
121+
DEVTOOLS_PORT: Final[int] = _get_environ_port("TEXTUAL_DEVTOOLS_PORT", 8081)
93122
"""Constant with the port that the devtools will connect to."""
94123

95-
SCREENSHOT_DELAY: Final[int] = _get_environ_int("TEXTUAL_SCREENSHOT", -1)
96-
"""Seconds delay before taking screenshot."""
124+
SCREENSHOT_DELAY: Final[int] = _get_environ_int("TEXTUAL_SCREENSHOT", -1, minimum=-1)
125+
"""Seconds delay before taking screenshot, -1 for no screenshot."""
97126

98127
SCREENSHOT_LOCATION: Final[str | None] = get_environ("TEXTUAL_SCREENSHOT_LOCATION")
99128
"""The location where screenshots should be written."""
@@ -107,7 +136,7 @@ def _get_textual_animations() -> AnimationLevel:
107136
SHOW_RETURN: Final[bool] = _get_environ_bool("TEXTUAL_SHOW_RETURN")
108137
"""Write the return value on exit."""
109138

110-
MAX_FPS: Final[int] = _get_environ_int("TEXTUAL_FPS", 60)
139+
MAX_FPS: Final[int] = _get_environ_int("TEXTUAL_FPS", 60, minimum=1)
111140
"""Maximum frames per second for updates."""
112141

113142
COLOR_SYSTEM: Final[str | None] = get_environ("TEXTUAL_COLOR_SYSTEM", "auto")
@@ -116,10 +145,10 @@ def _get_textual_animations() -> AnimationLevel:
116145
TEXTUAL_ANIMATIONS: Final[AnimationLevel] = _get_textual_animations()
117146
"""Determines whether animations run or not."""
118147

119-
ESCAPE_DELAY: Final[float] = _get_environ_int("ESCDELAY", 100) / 1000.0
148+
ESCAPE_DELAY: Final[float] = _get_environ_int("ESCDELAY", 100, minimum=1) / 1000.0
120149
"""The delay (in seconds) before reporting an escape key (not used if the extend key protocol is available)."""
121150

122-
SLOW_THRESHOLD: int = _get_environ_int("TEXTUAL_SLOW_THRESHOLD", 500)
151+
SLOW_THRESHOLD: int = _get_environ_int("TEXTUAL_SLOW_THRESHOLD", 500, minimum=100)
123152
"""The time threshold (in milliseconds) after which a warning is logged
124153
if message processing exceeds this duration.
125154
"""

tests/test_constants.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from textual.constants import _get_environ_bool, _get_environ_int, _get_environ_port
2+
3+
4+
def test_environ_int(monkeypatch):
5+
"""Check minimum is applied."""
6+
monkeypatch.setenv("FOO", "-1")
7+
assert _get_environ_int("FOO", 1, minimum=0) == 0
8+
monkeypatch.setenv("FOO", "0")
9+
assert _get_environ_int("FOO", 1, minimum=0) == 0
10+
monkeypatch.setenv("FOO", "1")
11+
assert _get_environ_int("FOO", 1, minimum=0) == 1
12+
13+
14+
def test_environ_bool(monkeypatch):
15+
"""Anything other than "1" is False."""
16+
monkeypatch.setenv("BOOL", "1")
17+
assert _get_environ_bool("BOOL") is True
18+
monkeypatch.setenv("BOOL", "")
19+
assert _get_environ_bool("BOOL") is False
20+
monkeypatch.setenv("BOOL", "0")
21+
assert _get_environ_bool("BOOL") is False
22+
23+
24+
def test_environ_port(monkeypatch):
25+
"""Valid ports are between 0 and 65536."""
26+
monkeypatch.setenv("PORT", "-1")
27+
assert _get_environ_port("PORT", 80) == 80
28+
monkeypatch.setenv("PORT", "0")
29+
assert _get_environ_port("PORT", 80) == 0
30+
monkeypatch.setenv("PORT", "65536")
31+
assert _get_environ_port("PORT", 80) == 80

0 commit comments

Comments
 (0)