Skip to content

Commit 5ca35c6

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 c00ab77 commit 5ca35c6

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/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`.
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 json import JSONDecodeError
1213

@@ -54,9 +55,6 @@
5455
}
5556

5657

57-
# Per-function deprecation. The module-level warning above tells users that
58-
# `nwis` overall is being phased out; these replacements tell them which
59-
# `waterdata` function to migrate each call to. Scheduled removal: 2027-05-06.
6058
_NWIS_REMOVAL_DATE = "2027-05-06"
6159
_REPLACEMENTS = {
6260
"get_dv": "`waterdata.get_daily()`",
@@ -71,21 +69,11 @@
7169
"query_waterservices": "a high-level `waterdata.get_*()` helper",
7270
}
7371

72+
_deprecation_state = threading.local()
7473

75-
def _warn_deprecated(func_name: str) -> None:
76-
"""Emit a per-function DeprecationWarning pointing at the waterdata replacement.
7774

78-
Suppresses the warning when invoked from another deprecated nwis function so
79-
that wrappers like ``get_record`` -> ``get_iv`` -> ``query_waterservices``
80-
surface only the outermost call (otherwise one user call produces three
81-
near-identical messages).
82-
"""
83-
module_globals = globals()
84-
frame = sys._getframe(2) if hasattr(sys, "_getframe") else None
85-
while frame is not None:
86-
if frame.f_globals is module_globals and frame.f_code.co_name in _REPLACEMENTS:
87-
return
88-
frame = frame.f_back
75+
def _warn_deprecated(func_name: str) -> None:
76+
"""Emit a per-function DeprecationWarning pointing at the waterdata replacement."""
8977
warnings.warn(
9078
f"`nwis.{func_name}` is deprecated and will be removed from "
9179
f"`dataretrieval` on or after {_NWIS_REMOVAL_DATE}; "
@@ -95,6 +83,33 @@ def _warn_deprecated(func_name: str) -> None:
9583
)
9684

9785

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+
98113
def _parse_json_or_raise(response: requests.Response) -> pd.DataFrame:
99114
"""Parse a JSON NWIS response, raising a helpful error on HTML responses."""
100115
try:
@@ -205,6 +220,7 @@ def get_discharge_measurements(**kwargs):
205220
)
206221

207222

223+
@_deprecated
208224
def get_discharge_peaks(
209225
sites: list[str] | str | None = None,
210226
start: str | None = None,
@@ -258,7 +274,6 @@ def get_discharge_peaks(
258274
... )
259275
260276
"""
261-
_warn_deprecated("get_discharge_peaks")
262277
_check_sites_value_types(sites)
263278

264279
kwargs["site_no"] = kwargs.pop("site_no", sites)
@@ -283,6 +298,7 @@ def get_gwlevels(**kwargs):
283298
)
284299

285300

301+
@_deprecated
286302
def get_stats(
287303
sites: list[str] | str | None = None, ssl_check: bool = True, **kwargs
288304
) -> tuple[pd.DataFrame, BaseMetadata]:
@@ -335,7 +351,6 @@ def get_stats(
335351
... )
336352
337353
"""
338-
_warn_deprecated("get_stats")
339354
_check_sites_value_types(sites)
340355

341356
response = query_waterservices(
@@ -345,6 +360,7 @@ def get_stats(
345360
return _read_rdb(response.text), NWIS_Metadata(response, **kwargs)
346361

347362

363+
@_deprecated
348364
def query_waterdata(
349365
service: str, ssl_check: bool = True, **kwargs
350366
) -> requests.models.Response:
@@ -366,7 +382,6 @@ def query_waterdata(
366382
request: ``requests.models.Response``
367383
The response object from the API request to the web service
368384
"""
369-
_warn_deprecated("query_waterdata")
370385
major_params = ["site_no", "state_cd"]
371386
bbox_params = [
372387
"nw_longitude_va",
@@ -391,6 +406,7 @@ def query_waterdata(
391406
return query(url, payload=kwargs, ssl_check=ssl_check)
392407

393408

409+
@_deprecated
394410
def query_waterservices(
395411
service: str, ssl_check: bool = True, **kwargs
396412
) -> requests.models.Response:
@@ -436,7 +452,6 @@ def query_waterservices(
436452
The response object from the API request to the web service
437453
438454
"""
439-
_warn_deprecated("query_waterservices")
440455
if not any(
441456
key in kwargs for key in ["sites", "stateCd", "bBox", "huc", "countyCd"]
442457
):
@@ -455,6 +470,7 @@ def query_waterservices(
455470
return query(url, payload=kwargs, ssl_check=ssl_check)
456471

457472

473+
@_deprecated
458474
def get_dv(
459475
sites: list[str] | str | None = None,
460476
start: str | None = None,
@@ -513,7 +529,6 @@ def get_dv(
513529
>>> df, md = dataretrieval.nwis.get_dv(sites="01646500")
514530
515531
"""
516-
_warn_deprecated("get_dv")
517532
_check_sites_value_types(sites)
518533

519534
kwargs["startDT"] = kwargs.pop("startDT", start)
@@ -527,6 +542,7 @@ def get_dv(
527542
return format_response(df, **kwargs), NWIS_Metadata(response, **kwargs)
528543

529544

545+
@_deprecated
530546
def get_info(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetadata]:
531547
"""
532548
Get site description information from NWIS.
@@ -619,7 +635,6 @@ def get_info(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetada
619635
>>> df, md = dataretrieval.nwis.get_info(sites=["05114000", "09423350"])
620636
621637
"""
622-
_warn_deprecated("get_info")
623638
seriesCatalogOutput = kwargs.pop("seriesCatalogOutput", None)
624639
if seriesCatalogOutput in ["True", "TRUE", "true", True]:
625640
warnings.warn(
@@ -643,6 +658,7 @@ def get_info(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetada
643658
return _read_rdb(response.text), NWIS_Metadata(response, **kwargs)
644659

645660

661+
@_deprecated
646662
def get_iv(
647663
sites: list[str] | str | None = None,
648664
start: str | None = None,
@@ -698,7 +714,6 @@ def get_iv(
698714
... )
699715
700716
"""
701-
_warn_deprecated("get_iv")
702717
_check_sites_value_types(sites)
703718

704719
kwargs["startDT"] = kwargs.pop("startDT", start)
@@ -727,6 +742,7 @@ def get_water_use(**kwargs):
727742
raise NameError("`nwis.get_water_use` is defunct.")
728743

729744

745+
@_deprecated
730746
def get_ratings(
731747
site: str | None = None,
732748
file_type: str = "base",
@@ -768,7 +784,6 @@ def get_ratings(
768784
>>> df, md = dataretrieval.nwis.get_ratings(site="01594440")
769785
770786
"""
771-
_warn_deprecated("get_ratings")
772787
site = kwargs.pop("site_no", site)
773788

774789
payload = {}
@@ -785,6 +800,7 @@ def get_ratings(
785800
return _read_rdb(response.text), NWIS_Metadata(response, site_no=site)
786801

787802

803+
@_deprecated
788804
def what_sites(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMetadata]:
789805
"""
790806
Search NWIS for sites within a region with specific data.
@@ -817,15 +833,14 @@ def what_sites(ssl_check: bool = True, **kwargs) -> tuple[pd.DataFrame, BaseMeta
817833
... )
818834
819835
"""
820-
_warn_deprecated("what_sites")
821-
822836
response = query_waterservices(service="site", ssl_check=ssl_check, **kwargs)
823837

824838
df = _read_rdb(response.text)
825839

826840
return df, NWIS_Metadata(response, **kwargs)
827841

828842

843+
@_deprecated
829844
def get_record(
830845
sites: list[str] | str | None = None,
831846
start: str | None = None,
@@ -914,7 +929,6 @@ def get_record(
914929
... )
915930
916931
"""
917-
_warn_deprecated("get_record")
918932
_check_sites_value_types(sites)
919933

920934
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)