Skip to content

Commit 3a14116

Browse files
authored
fix(logging): change isdigit for int log level detection to directly try int(level) and use error level as fallback, add more testing (#27)
1 parent 8b3935a commit 3a14116

File tree

2 files changed

+332
-6
lines changed

2 files changed

+332
-6
lines changed

python/model_hosting_container_standards/logging_config.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@
33
import logging
44
import os
55
import sys
6+
from typing import Union
7+
8+
9+
def parse_level(level: str) -> Union[int, str]:
10+
"""Parse a log level string into a valid logging level.
11+
12+
Args:
13+
level: Log level string to parse.
14+
15+
Returns:
16+
Valid logging level.
17+
"""
18+
# Convert level to uppercase
19+
level = level.upper()
20+
21+
# Convert numeric log level string to int so `setLevel` can recognize it
22+
try:
23+
return int(level)
24+
except ValueError:
25+
return level
626

727

828
def get_logger(name: str = "model_hosting_container_standards") -> logging.Logger:
@@ -22,7 +42,7 @@ def get_logger(name: str = "model_hosting_container_standards") -> logging.Logge
2242
# Convert level to uppercase
2343
level = os.getenv(
2444
"SAGEMAKER_CONTAINER_LOG_LEVEL", os.getenv("LOG_LEVEL", "ERROR")
25-
).upper()
45+
)
2646

2747
# Set up handler with consistent format
2848
handler = logging.StreamHandler(sys.stdout)
@@ -32,10 +52,13 @@ def get_logger(name: str = "model_hosting_container_standards") -> logging.Logge
3252
handler.setFormatter(formatter)
3353
logger.addHandler(handler)
3454

35-
# Convert numeric log level string to int so `setLevel` can recognize it
36-
# Will raise error if the set log level is not registered in
37-
# logging.getLevelNamesMapping()
38-
logger.setLevel(int(level) if level.isdigit() else level.upper())
55+
try:
56+
# Will raise error if the set log level is not registered in
57+
# logging.getLevelNamesMapping()
58+
logger.setLevel(parse_level(level))
59+
except (ValueError, AttributeError, TypeError):
60+
# if setLevel with environment variable fails, default to ERROR
61+
logger.setLevel(logging.ERROR)
3962

4063
# Prevent propagation to avoid duplicate logs
4164
logger.propagate = False

python/tests/test_logging_config.py

Lines changed: 304 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
from unittest.mock import patch
66

7-
from model_hosting_container_standards.logging_config import get_logger
7+
from model_hosting_container_standards.logging_config import get_logger, parse_level
88

99

1010
class TestGetLogger:
@@ -161,3 +161,306 @@ def test_logger_propagate_false(self):
161161
assert test_logger.propagate is False
162162
finally:
163163
test_logger.handlers.clear()
164+
165+
def test_integer_log_level_10_debug(self):
166+
"""Test that integer log level 10 (DEBUG) works."""
167+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "10"}):
168+
test_logger = get_logger("test_int_10_logger")
169+
try:
170+
assert len(test_logger.handlers) > 0
171+
assert test_logger.level == logging.DEBUG
172+
finally:
173+
test_logger.handlers.clear()
174+
175+
def test_integer_log_level_20_info(self):
176+
"""Test that integer log level 20 (INFO) works."""
177+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "20"}):
178+
test_logger = get_logger("test_int_20_logger")
179+
try:
180+
assert len(test_logger.handlers) > 0
181+
assert test_logger.level == logging.INFO
182+
finally:
183+
test_logger.handlers.clear()
184+
185+
def test_integer_log_level_30_warning(self):
186+
"""Test that integer log level 30 (WARNING) works."""
187+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "30"}):
188+
test_logger = get_logger("test_int_30_logger")
189+
try:
190+
assert len(test_logger.handlers) > 0
191+
assert test_logger.level == logging.WARNING
192+
finally:
193+
test_logger.handlers.clear()
194+
195+
def test_integer_log_level_40_error(self):
196+
"""Test that integer log level 40 (ERROR) works."""
197+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "40"}):
198+
test_logger = get_logger("test_int_40_logger")
199+
try:
200+
assert len(test_logger.handlers) > 0
201+
assert test_logger.level == logging.ERROR
202+
finally:
203+
test_logger.handlers.clear()
204+
205+
def test_integer_log_level_50_critical(self):
206+
"""Test that integer log level 50 (CRITICAL) works."""
207+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "50"}):
208+
test_logger = get_logger("test_int_50_logger")
209+
try:
210+
assert len(test_logger.handlers) > 0
211+
assert test_logger.level == logging.CRITICAL
212+
finally:
213+
test_logger.handlers.clear()
214+
215+
def test_custom_log_level(self):
216+
"""Test that custom log levels can be set."""
217+
# Add a custom log level
218+
custom_level_name = "CUSTOM_LEVEL"
219+
custom_level_value = 25 # Between INFO (20) and WARNING (30)
220+
logging.addLevelName(custom_level_value, custom_level_name)
221+
222+
try:
223+
with patch.dict(
224+
os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": custom_level_name}
225+
):
226+
test_logger = get_logger("test_custom_level_logger")
227+
try:
228+
assert len(test_logger.handlers) > 0
229+
assert test_logger.level == custom_level_value
230+
finally:
231+
test_logger.handlers.clear()
232+
finally:
233+
# Clean up custom level
234+
del logging._levelToName[custom_level_value]
235+
del logging._nameToLevel[custom_level_name]
236+
237+
def test_custom_log_level_with_integer(self):
238+
"""Test that custom integer log levels work."""
239+
custom_level_value = 25 # Between INFO (20) and WARNING (30)
240+
241+
with patch.dict(
242+
os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": str(custom_level_value)}
243+
):
244+
test_logger = get_logger("test_custom_int_level_logger")
245+
try:
246+
assert len(test_logger.handlers) > 0
247+
assert test_logger.level == custom_level_value
248+
finally:
249+
test_logger.handlers.clear()
250+
251+
def test_mixed_case_log_level(self):
252+
"""Test that mixed case log levels work (e.g., 'Info', 'Warning')."""
253+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "Info"}):
254+
test_logger = get_logger("test_mixed_case_logger")
255+
try:
256+
assert len(test_logger.handlers) > 0
257+
assert test_logger.level == logging.INFO
258+
finally:
259+
test_logger.handlers.clear()
260+
261+
def test_critical_log_level(self):
262+
"""Test that CRITICAL log level works."""
263+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "CRITICAL"}):
264+
test_logger = get_logger("test_critical_logger")
265+
try:
266+
assert len(test_logger.handlers) > 0
267+
assert test_logger.level == logging.CRITICAL
268+
finally:
269+
test_logger.handlers.clear()
270+
271+
def test_notset_log_level(self):
272+
"""Test that NOTSET log level works."""
273+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "NOTSET"}):
274+
test_logger = get_logger("test_notset_logger")
275+
try:
276+
assert len(test_logger.handlers) > 0
277+
assert test_logger.level == logging.NOTSET
278+
finally:
279+
test_logger.handlers.clear()
280+
281+
def test_invalid_log_level_defaults_to_error(self):
282+
"""Test that an invalid log level defaults to ERROR instead of crashing."""
283+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "INVALID_LEVEL"}):
284+
test_logger = get_logger("test_invalid_logger")
285+
try:
286+
assert len(test_logger.handlers) > 0
287+
# Should default to ERROR when invalid level is provided
288+
assert test_logger.level == logging.ERROR
289+
finally:
290+
test_logger.handlers.clear()
291+
292+
def test_empty_log_level_defaults_to_error(self):
293+
"""Test that empty log level string defaults to ERROR."""
294+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": ""}):
295+
test_logger = get_logger("test_empty_logger")
296+
try:
297+
assert len(test_logger.handlers) > 0
298+
# Should default to ERROR when empty level is provided
299+
assert test_logger.level == logging.ERROR
300+
finally:
301+
test_logger.handlers.clear()
302+
303+
def test_whitespace_log_level_defaults_to_error(self):
304+
"""Test that whitespace log level defaults to ERROR."""
305+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": " "}):
306+
test_logger = get_logger("test_whitespace_logger")
307+
try:
308+
assert len(test_logger.handlers) > 0
309+
# Should default to ERROR when whitespace level is provided
310+
assert test_logger.level == logging.ERROR
311+
finally:
312+
test_logger.handlers.clear()
313+
314+
def test_special_characters_log_level_defaults_to_error(self):
315+
"""Test that log levels with special characters default to ERROR."""
316+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "DEBUG!@#"}):
317+
test_logger = get_logger("test_special_chars_logger")
318+
try:
319+
assert len(test_logger.handlers) > 0
320+
# Should default to ERROR for invalid level with special chars
321+
assert test_logger.level == logging.ERROR
322+
finally:
323+
test_logger.handlers.clear()
324+
325+
def test_negative_integer_log_level(self):
326+
"""Test that negative integer log levels work (Python logging allows them)."""
327+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "-10"}):
328+
test_logger = get_logger("test_negative_int_logger")
329+
try:
330+
assert len(test_logger.handlers) > 0
331+
# Negative numbers are technically valid in Python logging
332+
# -10 would be even more verbose than DEBUG (10)
333+
assert test_logger.level == -10
334+
finally:
335+
test_logger.handlers.clear()
336+
337+
def test_float_log_level_defaults_to_error(self):
338+
"""Test that float log level strings default to ERROR."""
339+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "10.5"}):
340+
test_logger = get_logger("test_float_logger")
341+
try:
342+
assert len(test_logger.handlers) > 0
343+
# Float strings won't pass int(<float>), will fail as string name
344+
assert test_logger.level == logging.ERROR
345+
finally:
346+
test_logger.handlers.clear()
347+
348+
def test_zero_log_level(self):
349+
"""Test that zero log level works (equivalent to NOTSET)."""
350+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "0"}):
351+
test_logger = get_logger("test_zero_logger")
352+
try:
353+
assert len(test_logger.handlers) > 0
354+
assert test_logger.level == logging.NOTSET
355+
finally:
356+
test_logger.handlers.clear()
357+
358+
def test_very_large_integer_log_level(self):
359+
"""Test that very large integer log levels work."""
360+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "100"}):
361+
test_logger = get_logger("test_large_int_logger")
362+
try:
363+
assert len(test_logger.handlers) > 0
364+
# Python logging accepts any integer
365+
assert test_logger.level == 100
366+
finally:
367+
test_logger.handlers.clear()
368+
369+
def test_lowercase_integer_string(self):
370+
"""Test that numeric strings are parsed correctly regardless of case."""
371+
with patch.dict(os.environ, {"SAGEMAKER_CONTAINER_LOG_LEVEL": "20"}):
372+
test_logger = get_logger("test_numeric_string_logger")
373+
try:
374+
assert len(test_logger.handlers) > 0
375+
assert test_logger.level == logging.INFO
376+
finally:
377+
test_logger.handlers.clear()
378+
379+
380+
class TestParseLevel:
381+
"""Test parse_level function."""
382+
383+
def test_parse_level_string_debug(self):
384+
"""Test parsing DEBUG string returns uppercase string."""
385+
result = parse_level("debug")
386+
assert result == "DEBUG"
387+
assert isinstance(result, str)
388+
389+
def test_parse_level_string_info(self):
390+
"""Test parsing INFO string returns uppercase string."""
391+
result = parse_level("info")
392+
assert result == "INFO"
393+
assert isinstance(result, str)
394+
395+
def test_parse_level_already_uppercase(self):
396+
"""Test parsing already uppercase string."""
397+
result = parse_level("WARNING")
398+
assert result == "WARNING"
399+
assert isinstance(result, str)
400+
401+
def test_parse_level_mixed_case(self):
402+
"""Test parsing mixed case string."""
403+
result = parse_level("CrItIcAl")
404+
assert result == "CRITICAL"
405+
assert isinstance(result, str)
406+
407+
def test_parse_level_integer_string_10(self):
408+
"""Test parsing integer string '10' returns int."""
409+
result = parse_level("10")
410+
assert result == 10
411+
assert isinstance(result, int)
412+
413+
def test_parse_level_integer_string_20(self):
414+
"""Test parsing integer string '20' returns int."""
415+
result = parse_level("20")
416+
assert result == 20
417+
assert isinstance(result, int)
418+
419+
def test_parse_level_negative_integer_string(self):
420+
"""Test parsing negative integer string returns int."""
421+
result = parse_level("-10")
422+
assert result == -10
423+
assert isinstance(result, int)
424+
425+
def test_parse_level_zero(self):
426+
"""Test parsing '0' returns int."""
427+
result = parse_level("0")
428+
assert result == 0
429+
assert isinstance(result, int)
430+
431+
def test_parse_level_large_integer(self):
432+
"""Test parsing large integer string."""
433+
result = parse_level("999")
434+
assert result == 999
435+
assert isinstance(result, int)
436+
437+
def test_parse_level_float_string(self):
438+
"""Test parsing float string fails and returns uppercase string."""
439+
result = parse_level("10.5")
440+
# Can't convert to int, so returns as uppercase string
441+
assert result == "10.5"
442+
assert isinstance(result, str)
443+
444+
def test_parse_level_invalid_string(self):
445+
"""Test parsing invalid string returns uppercase."""
446+
result = parse_level("invalid")
447+
assert result == "INVALID"
448+
assert isinstance(result, str)
449+
450+
def test_parse_level_empty_string(self):
451+
"""Test parsing empty string returns empty string."""
452+
result = parse_level("")
453+
assert result == ""
454+
assert isinstance(result, str)
455+
456+
def test_parse_level_whitespace(self):
457+
"""Test parsing whitespace returns uppercase whitespace."""
458+
result = parse_level(" ")
459+
assert result == " "
460+
assert isinstance(result, str)
461+
462+
def test_parse_level_special_characters(self):
463+
"""Test parsing string with special characters."""
464+
result = parse_level("debug!@#")
465+
assert result == "DEBUG!@#"
466+
assert isinstance(result, str)

0 commit comments

Comments
 (0)