Skip to content

Commit 18f831f

Browse files
thodson-usgsclaude
andauthored
Deprecate remaining active nwis functions ahead of 2027-05-06 removal (#271)
The module-level "use waterdata instead" warning has been firing on import for a while; this PR makes the migration guidance actionable by emitting a per-function DeprecationWarning that names the specific waterdata replacement the user should switch to. Every active nwis function has a waterdata replacement, so all nine of them are deprecated here: nwis.get_dv -> waterdata.get_daily() nwis.get_iv -> waterdata.get_continuous() nwis.get_info -> waterdata.get_monitoring_locations() nwis.what_sites -> waterdata.get_monitoring_locations() nwis.get_stats -> waterdata.get_stats_por() / waterdata.get_stats_date_range() nwis.get_discharge_peaks -> waterdata.get_peaks() nwis.get_ratings -> waterdata.get_ratings() nwis.get_record -> the appropriate waterdata.get_*() nwis.query_waterdata -> a high-level waterdata.get_*() helper nwis.query_waterservices -> a high-level waterdata.get_*() helper (get_qwdata, get_discharge_measurements, get_gwlevels, get_pmcodes, and get_water_use are already defunct and raise NameError.) Implementation follows the nadp deprecation template (#243): a small _REPLACEMENTS dict + a _warn_deprecated(func_name) helper called as the first line of each public function. stacklevel=3 makes the warning point at the caller's code, not the helper's frame. Removal date is set to 2027-05-06. Maintainer can adjust if desired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent fca3d6c commit 18f831f

3 files changed

Lines changed: 157 additions & 1 deletion

File tree

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
**05/06/2026:** Each remaining active function in `dataretrieval.nwis` now emits a per-function `DeprecationWarning` naming the `waterdata` replacement to migrate to (visible the first time users call each getter). The `nwis` module is scheduled for removal on or after **2027-05-06**.
2+
13
**05/06/2026:** Added `waterdata.get_ratings(...)` — wraps the new Water Data STAC catalog (`api.waterdata.usgs.gov/stac/v0/search`) for USGS stage-discharge rating curves. Returns parsed `exsa` / `base` / `corr` rating tables as a dict of DataFrames keyed by feature ID, or just the list of available STAC features when `download_and_parse=False`. Mirrors R's `read_waterdata_ratings`.
24

35
**05/06/2026:** Added `waterdata.get_field_measurements_metadata(...)` — wraps the OGC `field-measurements-metadata` collection. Returns one row per (location, parameter) field-measurement series describing its period of record, units, etc., without the underlying observations. Discrete-measurement analogue to `get_time_series_metadata`. Mirrors R's `read_waterdata_field_meta`.

dataretrieval/nwis.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
from __future__ import annotations
88

9+
import functools
10+
import threading
911
import warnings
1012
from json import JSONDecodeError
1113

@@ -53,6 +55,61 @@
5355
}
5456

5557

58+
_NWIS_REMOVAL_DATE = "2027-05-06"
59+
_REPLACEMENTS = {
60+
"get_dv": "`waterdata.get_daily()`",
61+
"get_iv": "`waterdata.get_continuous()`",
62+
"get_info": "`waterdata.get_monitoring_locations()`",
63+
"what_sites": "`waterdata.get_monitoring_locations()`",
64+
"get_stats": "`waterdata.get_stats_por()` or `waterdata.get_stats_date_range()`",
65+
"get_discharge_peaks": "`waterdata.get_peaks()`",
66+
"get_ratings": "`waterdata.get_ratings()`",
67+
"get_record": "the appropriate `waterdata.get_*()` for the service you need",
68+
"query_waterdata": "a high-level `waterdata.get_*()` helper",
69+
"query_waterservices": "a high-level `waterdata.get_*()` helper",
70+
}
71+
72+
_deprecation_state = threading.local()
73+
74+
75+
def _warn_deprecated(func_name: str) -> None:
76+
"""Emit a per-function DeprecationWarning pointing at the waterdata replacement."""
77+
warnings.warn(
78+
f"`nwis.{func_name}` is deprecated and will be removed from "
79+
f"`dataretrieval` on or after {_NWIS_REMOVAL_DATE}; "
80+
f"use {_REPLACEMENTS[func_name]} instead.",
81+
DeprecationWarning,
82+
stacklevel=3,
83+
)
84+
85+
86+
def _deprecated(func):
87+
"""Mark an nwis function as deprecated.
88+
89+
Wrappers like ``get_record`` -> ``get_iv`` -> ``query_waterservices`` would
90+
otherwise emit one warning per layer; the thread-local sentinel ensures the
91+
user sees only the outermost call's warning.
92+
"""
93+
if func.__name__ not in _REPLACEMENTS:
94+
raise RuntimeError(
95+
f"_REPLACEMENTS missing entry for {func.__name__!r}; "
96+
"add a `waterdata` replacement before applying @_deprecated."
97+
)
98+
99+
@functools.wraps(func)
100+
def wrapper(*args, **kwargs):
101+
if getattr(_deprecation_state, "active", False):
102+
return func(*args, **kwargs)
103+
_deprecation_state.active = True
104+
try:
105+
_warn_deprecated(func.__name__)
106+
return func(*args, **kwargs)
107+
finally:
108+
_deprecation_state.active = False
109+
110+
return wrapper
111+
112+
56113
def _parse_json_or_raise(response: requests.Response) -> pd.DataFrame:
57114
"""Parse a JSON NWIS response, raising a helpful error on HTML responses."""
58115
try:
@@ -163,6 +220,7 @@ def get_discharge_measurements(**kwargs):
163220
)
164221

165222

223+
@_deprecated
166224
def get_discharge_peaks(
167225
sites: list[str] | str | None = None,
168226
start: str | None = None,
@@ -240,6 +298,7 @@ def get_gwlevels(**kwargs):
240298
)
241299

242300

301+
@_deprecated
243302
def get_stats(
244303
sites: list[str] | str | None = None, ssl_check: bool = True, **kwargs
245304
) -> tuple[pd.DataFrame, BaseMetadata]:
@@ -301,6 +360,7 @@ def get_stats(
301360
return _read_rdb(response.text), NWIS_Metadata(response, **kwargs)
302361

303362

363+
@_deprecated
304364
def query_waterdata(
305365
service: str, ssl_check: bool = True, **kwargs
306366
) -> requests.models.Response:
@@ -346,6 +406,7 @@ def query_waterdata(
346406
return query(url, payload=kwargs, ssl_check=ssl_check)
347407

348408

409+
@_deprecated
349410
def query_waterservices(
350411
service: str, ssl_check: bool = True, **kwargs
351412
) -> requests.models.Response:
@@ -409,6 +470,7 @@ def query_waterservices(
409470
return query(url, payload=kwargs, ssl_check=ssl_check)
410471

411472

473+
@_deprecated
412474
def get_dv(
413475
sites: list[str] | str | None = None,
414476
start: str | None = None,
@@ -480,6 +542,7 @@ def get_dv(
480542
return format_response(df, **kwargs), NWIS_Metadata(response, **kwargs)
481543

482544

545+
@_deprecated
483546
def get_info(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetadata]:
484547
"""
485548
Get site description information from NWIS.
@@ -595,6 +658,7 @@ def get_info(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetada
595658
return _read_rdb(response.text), NWIS_Metadata(response, **kwargs)
596659

597660

661+
@_deprecated
598662
def get_iv(
599663
sites: list[str] | str | None = None,
600664
start: str | None = None,
@@ -678,6 +742,7 @@ def get_water_use(**kwargs):
678742
raise NameError("`nwis.get_water_use` is defunct.")
679743

680744

745+
@_deprecated
681746
def get_ratings(
682747
site: str | None = None,
683748
file_type: str = "base",
@@ -735,6 +800,7 @@ def get_ratings(
735800
return _read_rdb(response.text), NWIS_Metadata(response, site_no=site)
736801

737802

803+
@_deprecated
738804
def what_sites(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetadata]:
739805
"""
740806
Search NWIS for sites within a region with specific data.
@@ -767,14 +833,14 @@ def what_sites(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMeta
767833
... )
768834
769835
"""
770-
771836
response = query_waterservices(service="site", ssl_check=ssl_check, **kwargs)
772837

773838
df = _read_rdb(response.text)
774839

775840
return df, NWIS_Metadata(response, **kwargs)
776841

777842

843+
@_deprecated
778844
def get_record(
779845
sites: list[str] | str | None = None,
780846
start: str | None = None,

tests/nwis_test.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import json
3+
import warnings
34
from pathlib import Path
45
from unittest import mock
56

@@ -118,6 +119,93 @@ def test_preformat_peaks_response():
118119
# Removed defunct gwlevels tests.
119120

120121

122+
class TestDeprecationWarnings:
123+
"""Verify per-function DeprecationWarning fires with the right replacement.
124+
125+
The module-level "use waterdata instead" warning fires on import; these
126+
tests pin the function-specific replacements so users see actionable
127+
migration guidance the first time they call each NWIS getter.
128+
"""
129+
130+
@pytest.mark.parametrize(
131+
"func_name, replacement_substring",
132+
[
133+
("get_dv", "waterdata.get_daily"),
134+
("get_iv", "waterdata.get_continuous"),
135+
("get_info", "waterdata.get_monitoring_locations"),
136+
("what_sites", "waterdata.get_monitoring_locations"),
137+
("get_stats", "waterdata.get_stats_por"),
138+
("get_discharge_peaks", "waterdata.get_peaks"),
139+
("get_ratings", "waterdata.get_ratings"),
140+
("get_record", "waterdata.get_*"),
141+
("query_waterdata", "waterdata.get_*"),
142+
("query_waterservices", "waterdata.get_*"),
143+
],
144+
)
145+
def test_warn_message_includes_replacement(self, func_name, replacement_substring):
146+
"""Each deprecated function emits a warning naming the right replacement."""
147+
from dataretrieval.nwis import _NWIS_REMOVAL_DATE, _warn_deprecated
148+
149+
with pytest.warns(DeprecationWarning, match=func_name) as record:
150+
_warn_deprecated(func_name)
151+
message = str(record[0].message)
152+
assert replacement_substring in message
153+
assert _NWIS_REMOVAL_DATE in message
154+
155+
def test_get_iv_fires_deprecation_on_call(self, requests_mock):
156+
"""End-to-end: a real call routes through _warn_deprecated."""
157+
requests_mock.get(
158+
"https://waterservices.usgs.gov/nwis/iv",
159+
json={"value": {"timeSeries": []}},
160+
)
161+
with pytest.warns(DeprecationWarning, match="get_iv.*waterdata.get_continuous"):
162+
get_iv(sites="01491000")
163+
164+
def test_nested_calls_emit_one_warning(self, requests_mock):
165+
"""get_record(service='iv') wraps get_iv -> query_waterservices.
166+
167+
Without re-entrancy suppression the user would see 3 near-identical
168+
deprecation warnings for one call; pin the outermost-only contract.
169+
"""
170+
requests_mock.get(
171+
"https://waterservices.usgs.gov/nwis/iv",
172+
json={"value": {"timeSeries": []}},
173+
)
174+
with warnings.catch_warnings(record=True) as caught:
175+
warnings.simplefilter("always", DeprecationWarning)
176+
get_record(sites="01491000", service="iv")
177+
deprecations = [w for w in caught if issubclass(w.category, DeprecationWarning)]
178+
assert len(deprecations) == 1
179+
assert "get_record" in str(deprecations[0].message)
180+
181+
@pytest.mark.parametrize(
182+
"name",
183+
[
184+
"get_daily",
185+
"get_continuous",
186+
"get_monitoring_locations",
187+
"get_stats_por",
188+
"get_stats_date_range",
189+
"get_peaks",
190+
"get_ratings",
191+
],
192+
)
193+
def test_named_replacement_exists_in_waterdata(self, name):
194+
"""Tripwire: every concrete `waterdata.*` named in a deprecation message
195+
must actually exist, so a user following the migration guidance doesn't
196+
hit AttributeError.
197+
198+
Fails loudly if this PR ever lands before its referenced replacement
199+
does (e.g. before `get_peaks` from #267).
200+
"""
201+
import dataretrieval.waterdata as wd
202+
203+
assert callable(getattr(wd, name, None)), (
204+
f"`waterdata.{name}` is missing — fix `_REPLACEMENTS` in nwis.py "
205+
"or add the replacement before merging."
206+
)
207+
208+
121209
class TestDefunct:
122210
"""Verify that defunct functions raise NameError."""
123211

0 commit comments

Comments
 (0)