From e6848a7d85fc7f4ff792af977d0df44147959ca7 Mon Sep 17 00:00:00 2001 From: Bob Weigel Date: Thu, 16 Jul 2026 14:32:40 -0400 Subject: [PATCH 1/5] filter pandas warnings; improve logger --- hapiclient/hapitime.py | 5 +- hapiclient/log.py | 13 +++-- hapiclient/util.py | 87 ++++++++++++++-------------------- test/test_hapitime2datetime.py | 37 +++++++++++++++ 4 files changed, 86 insertions(+), 56 deletions(-) diff --git a/hapiclient/hapitime.py b/hapiclient/hapitime.py index 5c449e2..d97199f 100644 --- a/hapiclient/hapitime.py +++ b/hapiclient/hapitime.py @@ -1,6 +1,7 @@ """Functions for manipulating HAPI times (restricted ISO 8601 strings).""" import re import time +import warnings import pandas import isodate @@ -268,7 +269,9 @@ def hapitime2datetime(Time, **kwargs): if pandas_major_version < 2: Time = pandas.to_datetime(Time, infer_datetime_format=True).tz_convert(tzinfo).to_pydatetime() else: - Time = pandas.to_datetime(Time).tz_convert(tzinfo).to_pydatetime() + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', message='Could not infer format') + Time = pandas.to_datetime(Time).tz_convert(tzinfo).to_pydatetime() if reshape: Time = np.reshape(Time, shape) toc = time.time() - tic diff --git a/hapiclient/log.py b/hapiclient/log.py index 2def7be..7df7b88 100644 --- a/hapiclient/log.py +++ b/hapiclient/log.py @@ -5,9 +5,14 @@ _INTERNAL_HANDLER_ATTR = "_hapiclient_internal_handler" _INTERNAL_LEVEL_ATTR = "_hapiclient_internal_level" -# Disable propagation by default so hapiclient logs don't bubble up -# to root logger (e.g., pytest's logger). Can be re-enabled by user. -_logger.propagate = False +# Per Python library best practices, add a NullHandler and leave propagation +# enabled so the application controls output. When running under pytest, +# disable propagation to prevent hapiclient INFO messages from leaking into +# pytest's captured log output. +import sys as _sys +_logger.addHandler(_logging.NullHandler()) +_logger.propagate = "pytest" not in _sys.modules +del _sys def configure_logging(logging): @@ -26,7 +31,7 @@ def configure_logging(logging): if logging is True: _logger.setLevel(_logging.INFO) setattr(_logger, _INTERNAL_LEVEL_ATTR, _logging.INFO) - _logger.propagate = False + _logger.propagate = False # use our own handler when logging=True _has_internal = any(getattr(h, _INTERNAL_HANDLER_ATTR, False) for h in _logger.handlers) if not _has_internal: import sys diff --git a/hapiclient/util.py b/hapiclient/util.py index c457632..13d91fc 100644 --- a/hapiclient/util.py +++ b/hapiclient/util.py @@ -104,80 +104,65 @@ def unicode_error_message(name): return msg -def warning_test(): - """For testing warning function.""" - - # Should show warnings in order and only HAPIWarning {1,2} should - # have a different format - from warnings import warn - - warn('Normal warning 1') - warn('Normal warning 2') - - warning('HAPI Warning 1') - warning('HAPI Warning 2') - - warn('Normal warning 3') - warn('Normal warning 4') +class HAPIWarning(Warning): + pass def warning(*args): """Display a short warning message. warning(message) raises a warning of type HAPIWarning and displays - "Warning: " + message. Use for warnings when a full stack trace is not + "HAPIWarning: " + message. Use for warnings when a full stack trace is not needed. """ + import logging import warnings - from os import path - from sys import stderr - from inspect import stack + import platform message = args[0] - if len(args) > 1: - fname = args[1] - else: - fname = stack()[1][1] - #line = stack()[1][2] + if logging.getLogger("hapiclient").level == logging.DEBUG: + warnings.warn(message, stacklevel=2) + return - fname = path.basename(fname) + _prefix = "\x1b[31mHAPIWarning:\x1b[0m " + if platform.system() == 'Windows' and pythonshell() == 'shell': + _prefix = "HAPIWarning: " - def prefix(): - import platform - prefix = "\x1b[31mHAPIWarning:\x1b[0m " - if platform.system() == 'Windows' and pythonshell() == 'shell': - prefix = "HAPIWarning: " + _orig_formatwarning = warnings.formatwarning - return prefix + def _formatwarning(msg, category, filename, lineno, line=None): + if issubclass(category, HAPIWarning): + return _prefix + str(msg) + "\n" + return _orig_formatwarning(msg, category, filename, lineno, line) - # Custom warning format function - def _warning(message, category=UserWarning, filename='', lineno=-1, file=None, line=''): - if category.__name__ == "HAPIWarning": - stderr.write(prefix() + str(message) + "\n") - else: - # Use default showwarning function. - showwarning_default(message, category=UserWarning, - filename='', lineno=-1, - file=None, line='') + warnings.formatwarning = _formatwarning + try: + warnings.warn(message, HAPIWarning, stacklevel=2) + finally: + warnings.formatwarning = _orig_formatwarning - stderr.flush() - # Reset showwarning function to default - warnings.showwarning = showwarning_default +def warning_test(): + """For testing warning function.""" - class HAPIWarning(Warning): - pass + # Should show warnings in order and only HAPIWarning {1,2} should + # have a different format + from warnings import warn + + warn('Normal warning 1') + warn('Normal warning 2') - # Copy default showwarning function - showwarning_default = warnings.showwarning + warning('HAPI Warning 1') + warning('HAPI Warning 2') + + warn('Normal warning 3') + warn('Normal warning 4') - # Use custom warning function instead of default - warnings.showwarning = _warning - # Raise warning - warnings.warn(message, HAPIWarning) +if __name__ == "__main__": + warning_test() class HAPIError(Exception): diff --git a/test/test_hapitime2datetime.py b/test/test_hapitime2datetime.py index b8fb272..45e2b01 100644 --- a/test/test_hapitime2datetime.py +++ b/test/test_hapitime2datetime.py @@ -84,6 +84,7 @@ def test_parsing(): def test_error_conditions(): from hapiclient import HAPIError + logger.info("test_error_conditions()") Time = "1999" @@ -96,7 +97,43 @@ def test_error_conditions(): assert False, "HAPIError not raised for hapitime2datetime(" + str(Time) + ")." + Time = "2001-01-01T00:00:00" + logger.info(" Checking that hapitime2datetime('" + str(Time) + "', allow_missing_Z=False) throws HAPIError") + try: + hapitime2datetime(Time, allow_missing_Z=False) + except HAPIError: + pass + else: + assert False, "HAPIError not raised for hapitime2datetime(" + str(Time) + ", allow_missing_Z=False)." + + +def test_warning_conditions(): + + import io + import logging + + logger.info("test_warning_conditions()") + + stream = io.StringIO() + handler = logging.StreamHandler(stream) + logger2 = logging.getLogger("hapiclient") + logger2.setLevel(logging.DEBUG) + logger2.addHandler(handler) + logger2.propagate = False + + try: + Time = "2001-001T00:00:00Z" + hapitime2datetime(Time) + output = stream.getvalue() + assert "Pandas processing failed with error" in output, \ + f"Expected 'Pandas processing failed with error' in log output, got:\n{output}" + finally: + logger2.removeHandler(handler) + logger2.propagate = True + + if __name__ == '__main__': test_api() test_parsing() test_error_conditions() + test_warning_conditions() From bdf325b4cd97d2c7ba8de9adf1d2a425ebc00509 Mon Sep 17 00:00:00 2001 From: Bob Weigel Date: Thu, 16 Jul 2026 14:50:23 -0400 Subject: [PATCH 2/5] more logging issues --- hapiclient/log.py | 4 +++- misc/hapi_logging_demo.py | 9 ++------- test/test_cache.py | 14 +++++++------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/hapiclient/log.py b/hapiclient/log.py index 7df7b88..e139ff6 100644 --- a/hapiclient/log.py +++ b/hapiclient/log.py @@ -26,7 +26,7 @@ def configure_logging(logging): has_user_handlers = any( not getattr(handler, _INTERNAL_HANDLER_ATTR, False) for handler in _logger.handlers - ) + ) or bool(_logging.root.handlers) if logging is True: _logger.setLevel(_logging.INFO) @@ -39,8 +39,10 @@ def configure_logging(logging): _handler.setFormatter(_logging.Formatter("%(message)s")) setattr(_handler, _INTERNAL_HANDLER_ATTR, True) _logger.addHandler(_handler) + if logging is False: if has_user_level or has_user_handlers: + _logger.propagate = True # ensure messages reach the user-configured handler #from .util import warning if has_user_handlers: pass diff --git a/misc/hapi_logging_demo.py b/misc/hapi_logging_demo.py index d859c2d..a0998aa 100644 --- a/misc/hapi_logging_demo.py +++ b/misc/hapi_logging_demo.py @@ -43,13 +43,8 @@ def reset_hapiclient_logging(): # 2. Log to console using Python's standard logging module if 4 <= method <= 6: import logging - logger = logging.getLogger("hapiclient") - logger.setLevel(logging.INFO) - handler = logging.StreamHandler() - formatter = logging.Formatter("%(asctime)s [%(name)s] %(message)s", datefmt="%Y-%m-%dT%H:%M:%S") - formatter.default_msec_format = '%s.%03d' - handler.setFormatter(formatter) - logger.addHandler(handler) + + logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s", datefmt="%Y-%m-%dT%H:%M:%S") if method == 4: # Use standard logging because defined diff --git a/test/test_cache.py b/test/test_cache.py index 32ef6fd..d5403b3 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -20,6 +20,8 @@ start = '1970-01-01' stop = '1970-01-01T00:00:03' +import pytest + def test_cache_short(): # Compare read with empty cache with read with hot cache and usecache=True @@ -36,23 +38,21 @@ def test_cache_short(): assert compare.equal(data, data2) +@pytest.mark.filterwarnings("ignore::hapiclient.util.HAPIWarning") def test_cache_error(): from unittest.mock import patch - import io - import contextlib import pathlib import tempfile from hapiclient.util import write_atomic + from hapiclient.util import HAPIWarning + def assert_warns(fn, expected): - buf = io.StringIO() - with contextlib.redirect_stderr(buf): + import pytest + with pytest.warns(HAPIWarning, match=expected): result = fn() - print(buf.getvalue()) - msg = f"Expected '{expected}' in stderr: {buf.getvalue()!r}" - assert expected in buf.getvalue(), msg return result # Direct calls to write_atomic() From 5dab7f75bebb16cb20b5893812edd70bf9c3d647 Mon Sep 17 00:00:00 2001 From: Bob Weigel Date: Thu, 16 Jul 2026 14:52:24 -0400 Subject: [PATCH 3/5] update logs --- test/test_cache.log | 0 test/test_hapitime2datetime.log | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 test/test_cache.log create mode 100644 test/test_hapitime2datetime.log diff --git a/test/test_cache.log b/test/test_cache.log new file mode 100644 index 0000000..e69de29 diff --git a/test/test_hapitime2datetime.log b/test/test_hapitime2datetime.log new file mode 100644 index 0000000..292fb70 --- /dev/null +++ b/test/test_hapitime2datetime.log @@ -0,0 +1,5 @@ +test_api() +test_error_conditions() + Checking that hapitime2datetime('1999') throws HAPIError + Checking that hapitime2datetime('2001-01-01T00:00:00', allow_missing_Z=False) throws HAPIError +test_warning_conditions() From 3aadf7e77ca06ea443ad6712e5324bed6996bb8e Mon Sep 17 00:00:00 2001 From: Bob Weigel Date: Mon, 20 Jul 2026 13:31:19 -0400 Subject: [PATCH 4/5] Update test_cache.py --- test/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_cache.py b/test/test_cache.py index d5403b3..3d5557f 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -1,5 +1,6 @@ # See ../README.md for instructions on running tests. import shutil +import pytest from hapiclient.hapi import hapi @@ -20,7 +21,6 @@ start = '1970-01-01' stop = '1970-01-01T00:00:03' -import pytest def test_cache_short(): From ba1af95d82a8fdcd6e05c9f5c33a6660f36f0d7c Mon Sep 17 00:00:00 2001 From: Bob Weigel Date: Wed, 22 Jul 2026 11:52:49 -0400 Subject: [PATCH 5/5] Update log.py --- hapiclient/log.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hapiclient/log.py b/hapiclient/log.py index e139ff6..9e25f2e 100644 --- a/hapiclient/log.py +++ b/hapiclient/log.py @@ -62,5 +62,7 @@ def log(msg, opts=None): # opts is not used but kept for backward compatibility import sys - pre = sys._getframe(1).f_code.co_name + '(): ' - _logger.info("hapiclient." + pre + msg) + frame = sys._getframe(1) + module = frame.f_globals.get('__name__', 'hapiclient').split('.')[0] + pre = frame.f_code.co_name + '(): ' + _logger.info(module + "." + pre + msg)