Skip to content

Commit 9528700

Browse files
thodson-usgsclaude
andcommitted
Switch to @_deprecated decorator; drop sys._getframe walk
Replaces nine `_warn_deprecated("name")` first-line calls with `@_deprecated` above the function definition. The decorator does three things: 1. Validates `func.__name__ in _REPLACEMENTS` at import time, so a missing mapping fails loudly instead of producing a KeyError on the first user call. 2. Drops the stringly-typed `_warn_deprecated("get_iv")` call sites — the name now flows from `func.__name__`, so a typo can't drift from the real symbol. 3. Replaces the `sys._getframe` stack walk with a thread-local re-entrancy flag. Because the decorator wraps the whole function call (not just the warn line), the flag's lifetime spans nested wrapper invocations correctly. No CPython implementation detail required. Tighten the NEWS.md entry and drop the unused `requests_mock` fixture from `test_warn_message_includes_replacement` (it never made an HTTP request). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cb26813 commit 9528700

3 files changed

Lines changed: 44 additions & 34 deletions

File tree

NEWS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
**05/06/2026:** Each remaining active function in `dataretrieval.nwis` now emits a per-function `DeprecationWarning` that names its `waterdata` replacement (`get_dv``get_daily`, `get_iv``get_continuous`, `get_info` / `what_sites``get_monitoring_locations`, `get_stats``get_stats_por` / `get_stats_date_range`, `get_discharge_peaks``get_peaks`, `get_ratings``get_ratings`, `get_record` / `query_waterdata` / `query_waterservices` → the appropriate `waterdata.get_*()` helper). The `nwis` module is scheduled for removal on or after **2027-05-06**.
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**.
22

33
**05/05/2026:** Added `waterdata.get_combined_metadata(...)` — wraps the Water Data API's `combined-metadata` collection, which joins the monitoring-locations catalog with the time-series-metadata catalog and returns one row per (location, parameter, statistic) inventory entry. This is the most flexible "what data is available" endpoint in the API: any location attribute (state, HUC, site type, drainage area, well-construction depth, …) can be combined with any time-series attribute (parameter code, statistic, data type, period of record, …) in a single query. Mirrors R's `read_waterdata_combined_meta`.
44

dataretrieval/nwis.py

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
from __future__ import annotations
88

9-
import sys
9+
import functools
10+
import threading
1011
import warnings
1112
from io import StringIO
1213
from json import JSONDecodeError
@@ -46,9 +47,6 @@
4647
_CRS = "EPSG:4269"
4748

4849

49-
# Per-function deprecation. The module-level warning above tells users that
50-
# `nwis` overall is being phased out; these replacements tell them which
51-
# `waterdata` function to migrate each call to. Scheduled removal: 2027-05-06.
5250
_NWIS_REMOVAL_DATE = "2027-05-06"
5351
_REPLACEMENTS = {
5452
"get_dv": "`waterdata.get_daily()`",
@@ -63,21 +61,11 @@
6361
"query_waterservices": "a high-level `waterdata.get_*()` helper",
6462
}
6563

64+
_deprecation_state = threading.local()
6665

67-
def _warn_deprecated(func_name: str) -> None:
68-
"""Emit a per-function DeprecationWarning pointing at the waterdata replacement.
6966

70-
Suppresses the warning when invoked from another deprecated nwis function so
71-
that wrappers like ``get_record`` -> ``get_iv`` -> ``query_waterservices``
72-
surface only the outermost call (otherwise one user call produces three
73-
near-identical messages).
74-
"""
75-
module_globals = globals()
76-
frame = sys._getframe(2) if hasattr(sys, "_getframe") else None
77-
while frame is not None:
78-
if frame.f_globals is module_globals and frame.f_code.co_name in _REPLACEMENTS:
79-
return
80-
frame = frame.f_back
67+
def _warn_deprecated(func_name: str) -> None:
68+
"""Emit a per-function DeprecationWarning pointing at the waterdata replacement."""
8169
warnings.warn(
8270
f"`nwis.{func_name}` is deprecated and will be removed from "
8371
f"`dataretrieval` on or after {_NWIS_REMOVAL_DATE}; "
@@ -87,6 +75,33 @@ def _warn_deprecated(func_name: str) -> None:
8775
)
8876

8977

78+
def _deprecated(func):
79+
"""Mark an nwis function as deprecated.
80+
81+
Wrappers like ``get_record`` -> ``get_iv`` -> ``query_waterservices`` would
82+
otherwise emit one warning per layer; the thread-local sentinel ensures the
83+
user sees only the outermost call's warning.
84+
"""
85+
if func.__name__ not in _REPLACEMENTS:
86+
raise RuntimeError(
87+
f"_REPLACEMENTS missing entry for {func.__name__!r}; "
88+
"add a `waterdata` replacement before applying @_deprecated."
89+
)
90+
91+
@functools.wraps(func)
92+
def wrapper(*args, **kwargs):
93+
if getattr(_deprecation_state, "active", False):
94+
return func(*args, **kwargs)
95+
_deprecation_state.active = True
96+
try:
97+
_warn_deprecated(func.__name__)
98+
return func(*args, **kwargs)
99+
finally:
100+
_deprecation_state.active = False
101+
102+
return wrapper
103+
104+
90105
def _parse_json_or_raise(response: requests.Response) -> pd.DataFrame:
91106
"""Parse a JSON NWIS response, raising a helpful error on HTML responses."""
92107
try:
@@ -197,6 +212,7 @@ def get_discharge_measurements(**kwargs):
197212
)
198213

199214

215+
@_deprecated
200216
def get_discharge_peaks(
201217
sites: list[str] | str | None = None,
202218
start: str | None = None,
@@ -250,7 +266,6 @@ def get_discharge_peaks(
250266
... )
251267
252268
"""
253-
_warn_deprecated("get_discharge_peaks")
254269
_check_sites_value_types(sites)
255270

256271
kwargs["site_no"] = kwargs.pop("site_no", sites)
@@ -275,6 +290,7 @@ def get_gwlevels(**kwargs):
275290
)
276291

277292

293+
@_deprecated
278294
def get_stats(
279295
sites: list[str] | str | None = None, ssl_check: bool = True, **kwargs
280296
) -> tuple[pd.DataFrame, BaseMetadata]:
@@ -327,7 +343,6 @@ def get_stats(
327343
... )
328344
329345
"""
330-
_warn_deprecated("get_stats")
331346
_check_sites_value_types(sites)
332347

333348
response = query_waterservices(
@@ -337,6 +352,7 @@ def get_stats(
337352
return _read_rdb(response.text), NWIS_Metadata(response, **kwargs)
338353

339354

355+
@_deprecated
340356
def query_waterdata(
341357
service: str, ssl_check: bool = True, **kwargs
342358
) -> requests.models.Response:
@@ -358,7 +374,6 @@ def query_waterdata(
358374
request: ``requests.models.Response``
359375
The response object from the API request to the web service
360376
"""
361-
_warn_deprecated("query_waterdata")
362377
major_params = ["site_no", "state_cd"]
363378
bbox_params = [
364379
"nw_longitude_va",
@@ -383,6 +398,7 @@ def query_waterdata(
383398
return query(url, payload=kwargs, ssl_check=ssl_check)
384399

385400

401+
@_deprecated
386402
def query_waterservices(
387403
service: str, ssl_check: bool = True, **kwargs
388404
) -> requests.models.Response:
@@ -428,7 +444,6 @@ def query_waterservices(
428444
The response object from the API request to the web service
429445
430446
"""
431-
_warn_deprecated("query_waterservices")
432447
if not any(
433448
key in kwargs for key in ["sites", "stateCd", "bBox", "huc", "countyCd"]
434449
):
@@ -447,6 +462,7 @@ def query_waterservices(
447462
return query(url, payload=kwargs, ssl_check=ssl_check)
448463

449464

465+
@_deprecated
450466
def get_dv(
451467
sites: list[str] | str | None = None,
452468
start: str | None = None,
@@ -505,7 +521,6 @@ def get_dv(
505521
>>> df, md = dataretrieval.nwis.get_dv(sites="01646500")
506522
507523
"""
508-
_warn_deprecated("get_dv")
509524
_check_sites_value_types(sites)
510525

511526
kwargs["startDT"] = kwargs.pop("startDT", start)
@@ -519,6 +534,7 @@ def get_dv(
519534
return format_response(df, **kwargs), NWIS_Metadata(response, **kwargs)
520535

521536

537+
@_deprecated
522538
def get_info(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetadata]:
523539
"""
524540
Get site description information from NWIS.
@@ -611,7 +627,6 @@ def get_info(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetada
611627
>>> df, md = dataretrieval.nwis.get_info(sites=["05114000", "09423350"])
612628
613629
"""
614-
_warn_deprecated("get_info")
615630
seriesCatalogOutput = kwargs.pop("seriesCatalogOutput", None)
616631
if seriesCatalogOutput in ["True", "TRUE", "true", True]:
617632
warnings.warn(
@@ -635,6 +650,7 @@ def get_info(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetada
635650
return _read_rdb(response.text), NWIS_Metadata(response, **kwargs)
636651

637652

653+
@_deprecated
638654
def get_iv(
639655
sites: list[str] | str | None = None,
640656
start: str | None = None,
@@ -690,7 +706,6 @@ def get_iv(
690706
... )
691707
692708
"""
693-
_warn_deprecated("get_iv")
694709
_check_sites_value_types(sites)
695710

696711
kwargs["startDT"] = kwargs.pop("startDT", start)
@@ -719,6 +734,7 @@ def get_water_use(**kwargs):
719734
raise NameError("`nwis.get_water_use` is defunct.")
720735

721736

737+
@_deprecated
722738
def get_ratings(
723739
site: str | None = None,
724740
file_type: str = "base",
@@ -760,7 +776,6 @@ def get_ratings(
760776
>>> df, md = dataretrieval.nwis.get_ratings(site="01594440")
761777
762778
"""
763-
_warn_deprecated("get_ratings")
764779
site = kwargs.pop("site_no", site)
765780

766781
payload = {}
@@ -777,6 +792,7 @@ def get_ratings(
777792
return _read_rdb(response.text), NWIS_Metadata(response, site_no=site)
778793

779794

795+
@_deprecated
780796
def what_sites(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetadata]:
781797
"""
782798
Search NWIS for sites within a region with specific data.
@@ -809,15 +825,14 @@ def what_sites(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMeta
809825
... )
810826
811827
"""
812-
_warn_deprecated("what_sites")
813-
814828
response = query_waterservices(service="site", ssl_check=ssl_check, **kwargs)
815829

816830
df = _read_rdb(response.text)
817831

818832
return df, NWIS_Metadata(response, **kwargs)
819833

820834

835+
@_deprecated
821836
def get_record(
822837
sites: list[str] | str | None = None,
823838
start: str | None = None,
@@ -906,7 +921,6 @@ def get_record(
906921
... )
907922
908923
"""
909-
_warn_deprecated("get_record")
910924
_check_sites_value_types(sites)
911925

912926
defunct_replacements = {

tests/nwis_test.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,14 +142,10 @@ class TestDeprecationWarnings:
142142
("query_waterservices", "waterdata.get_*"),
143143
],
144144
)
145-
def test_warn_message_includes_replacement(
146-
self, func_name, replacement_substring, requests_mock
147-
):
145+
def test_warn_message_includes_replacement(self, func_name, replacement_substring):
148146
"""Each deprecated function emits a warning naming the right replacement."""
149147
from dataretrieval.nwis import _NWIS_REMOVAL_DATE, _warn_deprecated
150148

151-
# Test the helper directly so we don't need to spin up a fake response
152-
# for every function. The integration is checked once below.
153149
with pytest.warns(DeprecationWarning, match=func_name) as record:
154150
_warn_deprecated(func_name)
155151
message = str(record[0].message)

0 commit comments

Comments
 (0)