Skip to content

Commit 1a320b5

Browse files
authored
103 date parsing warning (#104)
* filter pandas warnings; improve logger * more logging issues * update logs * Update test_cache.py * Update log.py
1 parent 268d388 commit 1a320b5

8 files changed

Lines changed: 107 additions & 73 deletions

File tree

hapiclient/hapitime.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Functions for manipulating HAPI times (restricted ISO 8601 strings)."""
22
import re
33
import time
4+
import warnings
45

56
import pandas
67
import isodate
@@ -268,7 +269,9 @@ def hapitime2datetime(Time, **kwargs):
268269
if pandas_major_version < 2:
269270
Time = pandas.to_datetime(Time, infer_datetime_format=True).tz_convert(tzinfo).to_pydatetime()
270271
else:
271-
Time = pandas.to_datetime(Time).tz_convert(tzinfo).to_pydatetime()
272+
with warnings.catch_warnings():
273+
warnings.filterwarnings('ignore', message='Could not infer format')
274+
Time = pandas.to_datetime(Time).tz_convert(tzinfo).to_pydatetime()
272275
if reshape:
273276
Time = np.reshape(Time, shape)
274277
toc = time.time() - tic

hapiclient/log.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
_INTERNAL_HANDLER_ATTR = "_hapiclient_internal_handler"
66
_INTERNAL_LEVEL_ATTR = "_hapiclient_internal_level"
77

8-
# Disable propagation by default so hapiclient logs don't bubble up
9-
# to root logger (e.g., pytest's logger). Can be re-enabled by user.
10-
_logger.propagate = False
8+
# Per Python library best practices, add a NullHandler and leave propagation
9+
# enabled so the application controls output. When running under pytest,
10+
# disable propagation to prevent hapiclient INFO messages from leaking into
11+
# pytest's captured log output.
12+
import sys as _sys
13+
_logger.addHandler(_logging.NullHandler())
14+
_logger.propagate = "pytest" not in _sys.modules
15+
del _sys
1116

1217

1318
def configure_logging(logging):
@@ -21,21 +26,23 @@ def configure_logging(logging):
2126
has_user_handlers = any(
2227
not getattr(handler, _INTERNAL_HANDLER_ATTR, False)
2328
for handler in _logger.handlers
24-
)
29+
) or bool(_logging.root.handlers)
2530

2631
if logging is True:
2732
_logger.setLevel(_logging.INFO)
2833
setattr(_logger, _INTERNAL_LEVEL_ATTR, _logging.INFO)
29-
_logger.propagate = False
34+
_logger.propagate = False # use our own handler when logging=True
3035
_has_internal = any(getattr(h, _INTERNAL_HANDLER_ATTR, False) for h in _logger.handlers)
3136
if not _has_internal:
3237
import sys
3338
_handler = _logging.StreamHandler(sys.stdout)
3439
_handler.setFormatter(_logging.Formatter("%(message)s"))
3540
setattr(_handler, _INTERNAL_HANDLER_ATTR, True)
3641
_logger.addHandler(_handler)
42+
3743
if logging is False:
3844
if has_user_level or has_user_handlers:
45+
_logger.propagate = True # ensure messages reach the user-configured handler
3946
#from .util import warning
4047
if has_user_handlers:
4148
pass
@@ -55,5 +62,7 @@ def log(msg, opts=None):
5562
# opts is not used but kept for backward compatibility
5663
import sys
5764

58-
pre = sys._getframe(1).f_code.co_name + '(): '
59-
_logger.info("hapiclient." + pre + msg)
65+
frame = sys._getframe(1)
66+
module = frame.f_globals.get('__name__', 'hapiclient').split('.')[0]
67+
pre = frame.f_code.co_name + '(): '
68+
_logger.info(module + "." + pre + msg)

hapiclient/util.py

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -104,80 +104,65 @@ def unicode_error_message(name):
104104
return msg
105105

106106

107-
def warning_test():
108-
"""For testing warning function."""
109-
110-
# Should show warnings in order and only HAPIWarning {1,2} should
111-
# have a different format
112-
from warnings import warn
113-
114-
warn('Normal warning 1')
115-
warn('Normal warning 2')
116-
117-
warning('HAPI Warning 1')
118-
warning('HAPI Warning 2')
119-
120-
warn('Normal warning 3')
121-
warn('Normal warning 4')
107+
class HAPIWarning(Warning):
108+
pass
122109

123110

124111
def warning(*args):
125112
"""Display a short warning message.
126113
127114
warning(message) raises a warning of type HAPIWarning and displays
128-
"Warning: " + message. Use for warnings when a full stack trace is not
115+
"HAPIWarning: " + message. Use for warnings when a full stack trace is not
129116
needed.
130117
"""
131118

119+
import logging
132120
import warnings
133-
from os import path
134-
from sys import stderr
135-
from inspect import stack
121+
import platform
136122

137123
message = args[0]
138-
if len(args) > 1:
139-
fname = args[1]
140-
else:
141-
fname = stack()[1][1]
142124

143-
#line = stack()[1][2]
125+
if logging.getLogger("hapiclient").level == logging.DEBUG:
126+
warnings.warn(message, stacklevel=2)
127+
return
144128

145-
fname = path.basename(fname)
129+
_prefix = "\x1b[31mHAPIWarning:\x1b[0m "
130+
if platform.system() == 'Windows' and pythonshell() == 'shell':
131+
_prefix = "HAPIWarning: "
146132

147-
def prefix():
148-
import platform
149-
prefix = "\x1b[31mHAPIWarning:\x1b[0m "
150-
if platform.system() == 'Windows' and pythonshell() == 'shell':
151-
prefix = "HAPIWarning: "
133+
_orig_formatwarning = warnings.formatwarning
152134

153-
return prefix
135+
def _formatwarning(msg, category, filename, lineno, line=None):
136+
if issubclass(category, HAPIWarning):
137+
return _prefix + str(msg) + "\n"
138+
return _orig_formatwarning(msg, category, filename, lineno, line)
154139

155-
# Custom warning format function
156-
def _warning(message, category=UserWarning, filename='', lineno=-1, file=None, line=''):
157-
if category.__name__ == "HAPIWarning":
158-
stderr.write(prefix() + str(message) + "\n")
159-
else:
160-
# Use default showwarning function.
161-
showwarning_default(message, category=UserWarning,
162-
filename='', lineno=-1,
163-
file=None, line='')
140+
warnings.formatwarning = _formatwarning
141+
try:
142+
warnings.warn(message, HAPIWarning, stacklevel=2)
143+
finally:
144+
warnings.formatwarning = _orig_formatwarning
164145

165-
stderr.flush()
166146

167-
# Reset showwarning function to default
168-
warnings.showwarning = showwarning_default
147+
def warning_test():
148+
"""For testing warning function."""
169149

170-
class HAPIWarning(Warning):
171-
pass
150+
# Should show warnings in order and only HAPIWarning {1,2} should
151+
# have a different format
152+
from warnings import warn
153+
154+
warn('Normal warning 1')
155+
warn('Normal warning 2')
172156

173-
# Copy default showwarning function
174-
showwarning_default = warnings.showwarning
157+
warning('HAPI Warning 1')
158+
warning('HAPI Warning 2')
159+
160+
warn('Normal warning 3')
161+
warn('Normal warning 4')
175162

176-
# Use custom warning function instead of default
177-
warnings.showwarning = _warning
178163

179-
# Raise warning
180-
warnings.warn(message, HAPIWarning)
164+
if __name__ == "__main__":
165+
warning_test()
181166

182167

183168
class HAPIError(Exception):

misc/hapi_logging_demo.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,8 @@ def reset_hapiclient_logging():
4343
# 2. Log to console using Python's standard logging module
4444
if 4 <= method <= 6:
4545
import logging
46-
logger = logging.getLogger("hapiclient")
47-
logger.setLevel(logging.INFO)
48-
handler = logging.StreamHandler()
49-
formatter = logging.Formatter("%(asctime)s [%(name)s] %(message)s", datefmt="%Y-%m-%dT%H:%M:%S")
50-
formatter.default_msec_format = '%s.%03d'
51-
handler.setFormatter(formatter)
52-
logger.addHandler(handler)
46+
47+
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s", datefmt="%Y-%m-%dT%H:%M:%S")
5348

5449
if method == 4:
5550
# Use standard logging because defined

test/test_cache.log

Whitespace-only changes.

test/test_cache.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# See ../README.md for instructions on running tests.
22
import shutil
3+
import pytest
34

45
from hapiclient.hapi import hapi
56

@@ -20,6 +21,7 @@
2021
start = '1970-01-01'
2122
stop = '1970-01-01T00:00:03'
2223

24+
2325
def test_cache_short():
2426

2527
# Compare read with empty cache with read with hot cache and usecache=True
@@ -36,23 +38,21 @@ def test_cache_short():
3638
assert compare.equal(data, data2)
3739

3840

41+
@pytest.mark.filterwarnings("ignore::hapiclient.util.HAPIWarning")
3942
def test_cache_error():
4043

4144
from unittest.mock import patch
4245

43-
import io
44-
import contextlib
4546
import pathlib
4647
import tempfile
4748
from hapiclient.util import write_atomic
4849

50+
from hapiclient.util import HAPIWarning
51+
4952
def assert_warns(fn, expected):
50-
buf = io.StringIO()
51-
with contextlib.redirect_stderr(buf):
53+
import pytest
54+
with pytest.warns(HAPIWarning, match=expected):
5255
result = fn()
53-
print(buf.getvalue())
54-
msg = f"Expected '{expected}' in stderr: {buf.getvalue()!r}"
55-
assert expected in buf.getvalue(), msg
5656
return result
5757

5858
# Direct calls to write_atomic()

test/test_hapitime2datetime.log

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
test_api()
2+
test_error_conditions()
3+
Checking that hapitime2datetime('1999') throws HAPIError
4+
Checking that hapitime2datetime('2001-01-01T00:00:00', allow_missing_Z=False) throws HAPIError
5+
test_warning_conditions()

test/test_hapitime2datetime.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def test_parsing():
8484

8585
def test_error_conditions():
8686
from hapiclient import HAPIError
87+
8788
logger.info("test_error_conditions()")
8889

8990
Time = "1999"
@@ -96,7 +97,43 @@ def test_error_conditions():
9697
assert False, "HAPIError not raised for hapitime2datetime(" + str(Time) + ")."
9798

9899

100+
Time = "2001-01-01T00:00:00"
101+
logger.info(" Checking that hapitime2datetime('" + str(Time) + "', allow_missing_Z=False) throws HAPIError")
102+
try:
103+
hapitime2datetime(Time, allow_missing_Z=False)
104+
except HAPIError:
105+
pass
106+
else:
107+
assert False, "HAPIError not raised for hapitime2datetime(" + str(Time) + ", allow_missing_Z=False)."
108+
109+
110+
def test_warning_conditions():
111+
112+
import io
113+
import logging
114+
115+
logger.info("test_warning_conditions()")
116+
117+
stream = io.StringIO()
118+
handler = logging.StreamHandler(stream)
119+
logger2 = logging.getLogger("hapiclient")
120+
logger2.setLevel(logging.DEBUG)
121+
logger2.addHandler(handler)
122+
logger2.propagate = False
123+
124+
try:
125+
Time = "2001-001T00:00:00Z"
126+
hapitime2datetime(Time)
127+
output = stream.getvalue()
128+
assert "Pandas processing failed with error" in output, \
129+
f"Expected 'Pandas processing failed with error' in log output, got:\n{output}"
130+
finally:
131+
logger2.removeHandler(handler)
132+
logger2.propagate = True
133+
134+
99135
if __name__ == '__main__':
100136
test_api()
101137
test_parsing()
102138
test_error_conditions()
139+
test_warning_conditions()

0 commit comments

Comments
 (0)