From 7c9e35456f6de2ea3fdbb6debbb30279af934d4a Mon Sep 17 00:00:00 2001 From: thodson-usgs Date: Mon, 13 Apr 2026 16:58:31 -0500 Subject: [PATCH 1/2] Fix IndexError in _read_rdb when NWIS returns no data rows When an NWIS service responds with only comment lines (e.g. "No sites found matching all criteria"), _read_rdb tried to index past the end of the lines list and raised an unhelpful IndexError. Now it detects the empty-data case, extracts the Response-Message from the comment block, and raises a ValueError with that message instead. Fixes #171. Co-Authored-By: Claude Sonnet 4.6 --- dataretrieval/nwis.py | 14 ++++++++++++-- tests/nwis_test.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/dataretrieval/nwis.py b/dataretrieval/nwis.py index bd8856e..74b7bc9 100644 --- a/dataretrieval/nwis.py +++ b/dataretrieval/nwis.py @@ -1039,8 +1039,9 @@ def _read_rdb(rdb): ) count = 0 + lines = rdb.splitlines() - for line in rdb.splitlines(): + for line in lines: # ignore comment lines if line.startswith("#"): count = count + 1 @@ -1048,7 +1049,16 @@ def _read_rdb(rdb): else: break - fields = rdb.splitlines()[count].split("\t") + if count >= len(lines): + # All lines are comments — no data rows. Extract the NWIS message. + msg = "No data returned from the NWIS service." + for line in lines: + if "Response-Message:" in line: + msg = line.split("Response-Message:")[-1].strip() + break + raise ValueError(msg) + + fields = lines[count].split("\t") fields = [field.replace(",", "").strip() for field in fields if field.strip()] dtypes = { "site_no": str, diff --git a/tests/nwis_test.py b/tests/nwis_test.py index b31f2c2..80ea664 100644 --- a/tests/nwis_test.py +++ b/tests/nwis_test.py @@ -9,6 +9,7 @@ from dataretrieval.nwis import ( NWIS_Metadata, + _read_rdb, get_discharge_measurements, get_gwlevels, get_info, @@ -321,3 +322,44 @@ def test_variable_info_deprecated(self): ): result = md.variable_info assert result is None + + +class TestReadRdb: + """Tests for the _read_rdb helper. + + Notes + ----- + Related to GitHub Issue #171. + """ + + # Minimal valid RDB response with one data row + _VALID_RDB = ( + "# comment\n" + "site_no\tvalue\n" + "5s\t10n\n" + "01491000\t42\n" + ) + + # NWIS response when no sites match the query criteria + _NO_SITES_RDB = ( + "# //Output-Format: RDB\n" + "# //Response-Status: OK\n" + "# //Response-Message: No sites found matching all criteria\n" + ) + + def test_valid_rdb_returns_dataframe(self): + """_read_rdb returns a DataFrame for a well-formed RDB response.""" + df = _read_rdb(self._VALID_RDB) + assert isinstance(df, pd.DataFrame) + assert "site_no" in df.columns + + def test_no_sites_raises_value_error(self): + """_read_rdb raises ValueError when NWIS returns no data rows (issue #171).""" + with pytest.raises(ValueError, match="No sites found"): + _read_rdb(self._NO_SITES_RDB) + + def test_empty_comments_raises_value_error(self): + """_read_rdb raises a generic ValueError when response has only comments.""" + rdb = "# just a comment\n# another comment\n" + with pytest.raises(ValueError): + _read_rdb(rdb) From 0ddb05c3d419e4f2916ceeae9499015e80ddf218 Mon Sep 17 00:00:00 2001 From: thodson-usgs Date: Mon, 13 Apr 2026 17:01:58 -0500 Subject: [PATCH 2/2] Return empty DataFrame from _read_rdb when NWIS finds no data When the NWIS service responds with only comment lines (e.g. "No sites found matching all criteria"), _read_rdb indexed past the end of the line list and raised an unhelpful IndexError. Since this is a legitimate empty result rather than an error, the fix returns an empty DataFrame so callers can use the idiomatic df.empty check instead of catching an exception. Fixes #171. Co-Authored-By: Claude Sonnet 4.6 --- dataretrieval/nwis.py | 11 ++++------- tests/nwis_test.py | 30 ++++++++++++++++-------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/dataretrieval/nwis.py b/dataretrieval/nwis.py index 74b7bc9..0bcb1d6 100644 --- a/dataretrieval/nwis.py +++ b/dataretrieval/nwis.py @@ -1050,13 +1050,10 @@ def _read_rdb(rdb): break if count >= len(lines): - # All lines are comments — no data rows. Extract the NWIS message. - msg = "No data returned from the NWIS service." - for line in lines: - if "Response-Message:" in line: - msg = line.split("Response-Message:")[-1].strip() - break - raise ValueError(msg) + # All lines are comments — the service returned no data rows (e.g. + # "No sites found matching all criteria"). This is a legitimate empty + # result, so return an empty DataFrame rather than raising. + return pd.DataFrame() fields = lines[count].split("\t") fields = [field.replace(",", "").strip() for field in fields if field.strip()] diff --git a/tests/nwis_test.py b/tests/nwis_test.py index 80ea664..c52775a 100644 --- a/tests/nwis_test.py +++ b/tests/nwis_test.py @@ -333,12 +333,7 @@ class TestReadRdb: """ # Minimal valid RDB response with one data row - _VALID_RDB = ( - "# comment\n" - "site_no\tvalue\n" - "5s\t10n\n" - "01491000\t42\n" - ) + _VALID_RDB = "# comment\nsite_no\tvalue\n5s\t10n\n01491000\t42\n" # NWIS response when no sites match the query criteria _NO_SITES_RDB = ( @@ -353,13 +348,20 @@ def test_valid_rdb_returns_dataframe(self): assert isinstance(df, pd.DataFrame) assert "site_no" in df.columns - def test_no_sites_raises_value_error(self): - """_read_rdb raises ValueError when NWIS returns no data rows (issue #171).""" - with pytest.raises(ValueError, match="No sites found"): - _read_rdb(self._NO_SITES_RDB) + def test_no_sites_returns_empty_dataframe(self): + """_read_rdb returns an empty DataFrame when NWIS finds no matching sites. + + A "No sites found" response is a legitimate empty result, not an error, + so callers can check ``df.empty`` rather than catching an exception. + Regression test for issue #171 (previously raised IndexError). + """ + df = _read_rdb(self._NO_SITES_RDB) + assert isinstance(df, pd.DataFrame) + assert df.empty - def test_empty_comments_raises_value_error(self): - """_read_rdb raises a generic ValueError when response has only comments.""" + def test_all_comments_returns_empty_dataframe(self): + """_read_rdb returns an empty DataFrame when the response has only comments.""" rdb = "# just a comment\n# another comment\n" - with pytest.raises(ValueError): - _read_rdb(rdb) + df = _read_rdb(rdb) + assert isinstance(df, pd.DataFrame) + assert df.empty