Skip to content

Commit e38faf5

Browse files
thodson-usgsclaude
andcommitted
Fix Watershed class: persist instance state and populate from get_watershed
`Watershed.from_streamstats_json` was a classmethod that assigned to `cls` instead of an instance, so every watershed shared class-level attributes and the method returned the class itself. `Watershed.__init__` called `get_watershed(...)` and discarded the result, so instances had no state of their own. And `get_watershed(format="object")` returned `None` rather than the parsed JSON the docstring promised. `from_streamstats_json` now builds a real instance, `__init__` requests `format="object"` and populates `self`, and `get_watershed(format="object")` returns the parsed dict. `_workspaceID` is preserved as a read-only alias of the new `workspace_id` attribute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 51ac674 commit e38faf5

2 files changed

Lines changed: 104 additions & 14 deletions

File tree

dataretrieval/streamstats.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -136,26 +136,47 @@ def get_watershed(
136136
# use Fiona to return a shape object
137137
pass
138138

139+
data = json.loads(r.text)
140+
139141
if format == "object":
140-
# return a python object
141-
pass
142+
return data
142143

143-
data = json.loads(r.text)
144144
return Watershed.from_streamstats_json(data)
145145

146146

147147
class Watershed:
148-
"""Class to extract information from the streamstats JSON object."""
148+
"""Class to extract information from the streamstats JSON object.
149149
150-
@classmethod
151-
def from_streamstats_json(cls, streamstats_json):
152-
"""Method that creates a Watershed object from a streamstats JSON."""
153-
cls.watershed_point = streamstats_json["featurecollection"][0]["feature"]
154-
cls.watershed_polygon = streamstats_json["featurecollection"][1]["feature"]
155-
cls.parameters = streamstats_json["parameters"]
156-
cls._workspaceID = streamstats_json["workspaceID"]
157-
return cls
150+
Attributes
151+
----------
152+
watershed_point : dict
153+
GeoJSON feature for the watershed pour point.
154+
watershed_polygon : dict
155+
GeoJSON feature for the delineated watershed polygon.
156+
parameters : list
157+
Watershed parameters returned by StreamStats.
158+
workspace_id : str
159+
StreamStats workspace identifier for the watershed.
160+
"""
158161

159162
def __init__(self, rcode, xlocation, ylocation):
160-
"""Init method that calls the :obj:`from_streamstats_json` method."""
161-
get_watershed(rcode, xlocation, ylocation)
163+
"""Delineate a watershed and populate the instance from the response."""
164+
data = get_watershed(rcode, xlocation, ylocation, format="object")
165+
self._populate_from_json(data)
166+
167+
@classmethod
168+
def from_streamstats_json(cls, streamstats_json):
169+
"""Construct a :obj:`Watershed` from a StreamStats JSON response."""
170+
instance = cls.__new__(cls)
171+
instance._populate_from_json(streamstats_json)
172+
return instance
173+
174+
def _populate_from_json(self, streamstats_json):
175+
self.watershed_point = streamstats_json["featurecollection"][0]["feature"]
176+
self.watershed_polygon = streamstats_json["featurecollection"][1]["feature"]
177+
self.parameters = streamstats_json["parameters"]
178+
self.workspace_id = streamstats_json["workspaceID"]
179+
180+
@property
181+
def _workspaceID(self):
182+
return self.workspace_id

tests/streamstats_test.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Tests for the streamstats module."""
2+
3+
import json
4+
5+
from dataretrieval.streamstats import Watershed, get_watershed
6+
7+
SAMPLE_JSON = {
8+
"featurecollection": [
9+
{"name": "globalwatershedpoint", "feature": {"type": "Feature", "id": "pt-1"}},
10+
{"name": "globalwatershed", "feature": {"type": "Feature", "id": "poly-1"}},
11+
],
12+
"parameters": [{"code": "DRNAREA", "value": 41.2}],
13+
"workspaceID": "NY20240101000000000",
14+
}
15+
16+
17+
def test_from_streamstats_json_returns_instance():
18+
"""Watershed.from_streamstats_json must return an instance, not the class."""
19+
w = Watershed.from_streamstats_json(SAMPLE_JSON)
20+
assert isinstance(w, Watershed)
21+
22+
23+
def test_from_streamstats_json_does_not_mutate_class():
24+
"""Two watersheds must not share state via class-level attributes."""
25+
other_json = {
26+
"featurecollection": [
27+
{"feature": {"id": "pt-2"}},
28+
{"feature": {"id": "poly-2"}},
29+
],
30+
"parameters": [{"code": "OTHER", "value": 1.0}],
31+
"workspaceID": "VT20240101000000000",
32+
}
33+
w1 = Watershed.from_streamstats_json(SAMPLE_JSON)
34+
w2 = Watershed.from_streamstats_json(other_json)
35+
36+
assert w1.workspace_id == "NY20240101000000000"
37+
assert w2.workspace_id == "VT20240101000000000"
38+
assert w1.parameters[0]["code"] == "DRNAREA"
39+
assert w2.parameters[0]["code"] == "OTHER"
40+
assert w1.watershed_point["id"] == "pt-1"
41+
assert w2.watershed_point["id"] == "pt-2"
42+
43+
44+
def test_get_watershed_object_returns_dict(requests_mock):
45+
"""get_watershed(format='object') must return parsed JSON, not None."""
46+
url = "https://streamstats.usgs.gov/streamstatsservices/watershed.geojson"
47+
requests_mock.get(url, text=json.dumps(SAMPLE_JSON))
48+
49+
result = get_watershed("NY", -74.524, 43.939, format="object")
50+
assert isinstance(result, dict)
51+
assert result["workspaceID"] == "NY20240101000000000"
52+
53+
54+
def test_watershed_init_populates_instance(requests_mock):
55+
"""Watershed(...) must populate the instance (regression: previously discarded)."""
56+
url = "https://streamstats.usgs.gov/streamstatsservices/watershed.geojson"
57+
requests_mock.get(url, text=json.dumps(SAMPLE_JSON))
58+
59+
w = Watershed("NY", -74.524, 43.939)
60+
assert w.workspace_id == "NY20240101000000000"
61+
assert w.parameters[0]["code"] == "DRNAREA"
62+
assert w.watershed_point["id"] == "pt-1"
63+
assert w.watershed_polygon["id"] == "poly-1"
64+
65+
66+
def test_workspace_id_back_compat_alias():
67+
"""Legacy `_workspaceID` attribute should still resolve."""
68+
w = Watershed.from_streamstats_json(SAMPLE_JSON)
69+
assert w._workspaceID == w.workspace_id == "NY20240101000000000"

0 commit comments

Comments
 (0)