Skip to content

Commit 5dbef21

Browse files
committed
Avoid caching failed ticker lookups
1 parent f132df7 commit 5dbef21

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

‎tests/test_lookup.py‎

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import json
12
import unittest
3+
from unittest import mock
24

35
import pandas as pd
46

@@ -98,5 +100,66 @@ def get(*args, **kwargs):
98100
self.assertIn(query, str(cm.exception))
99101

100102

103+
class TestLookupCache(unittest.TestCase):
104+
"""Lookup cache tests using mocked responses, without network requests."""
105+
106+
def setUp(self):
107+
self.lookup = yf.Lookup("AAPL")
108+
self.lookup._data = mock.Mock()
109+
110+
self.bad_response = mock.Mock(text="<html>Temporary error</html>")
111+
self.bad_response.json.side_effect = json.JSONDecodeError(
112+
"Expecting value", self.bad_response.text, 0)
113+
114+
self.good_response = mock.Mock(text="{}")
115+
self.good_response.json.return_value = {
116+
"finance": {
117+
"error": None,
118+
"result": [{"documents": [{"symbol": "AAPL", "shortName": "Apple Inc."}]}],
119+
}
120+
}
121+
122+
hide_exceptions = mock.patch.dict(yf.config.debug.data, {"hide_exceptions": True})
123+
hide_exceptions.start()
124+
self.addCleanup(hide_exceptions.stop)
125+
126+
def test_lookup_recovers_after_invalid_json(self):
127+
self.lookup._data.get.side_effect = [self.bad_response, self.good_response]
128+
129+
self.assertTrue(self.lookup.get_stock(count=5).empty)
130+
result = self.lookup.get_stock(count=5)
131+
self.assertEqual(result.index.tolist(), ["AAPL"])
132+
self.assertEqual(result.loc["AAPL", "shortName"], "Apple Inc.")
133+
134+
# Once a valid response arrives, subsequent calls should use the cache.
135+
pd.testing.assert_frame_equal(self.lookup.get_stock(count=5), result)
136+
self.assertEqual(self.lookup._data.get.call_count, 2)
137+
138+
def test_repeated_invalid_json_is_not_cached(self):
139+
self.lookup._data.get.return_value = self.bad_response
140+
141+
self.assertTrue(self.lookup.get_stock(count=5).empty)
142+
self.assertTrue(self.lookup.get_stock(count=5).empty)
143+
self.assertEqual(self.lookup._data.get.call_count, 2)
144+
145+
def test_valid_empty_results_are_cached(self):
146+
self.good_response.json.return_value["finance"]["result"][0]["documents"] = []
147+
self.lookup._data.get.return_value = self.good_response
148+
149+
self.assertTrue(self.lookup.get_stock(count=5).empty)
150+
self.assertTrue(self.lookup.get_stock(count=5).empty)
151+
self.assertEqual(self.lookup._data.get.call_count, 1)
152+
153+
def test_invalid_json_still_raises_when_exceptions_are_visible(self):
154+
self.lookup._data.get.side_effect = [self.bad_response, self.good_response]
155+
156+
with mock.patch.dict(yf.config.debug.data, {"hide_exceptions": False}):
157+
with self.assertRaises(json.JSONDecodeError):
158+
self.lookup.get_stock(count=5)
159+
160+
self.assertEqual(self.lookup.get_stock(count=5).index.tolist(), ["AAPL"])
161+
self.assertEqual(self.lookup._data.get.call_count, 2)
162+
163+
101164
if __name__ == "__main__":
102165
unittest.main()

‎yfinance/lookup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def _fetch_lookup(self, lookup_type="all", count=25) -> dict:
8383
if not YfConfig.debug.hide_exceptions:
8484
raise
8585
self._logger.error(f"{self.query}: 'lookup' fetch received faulty data")
86-
data = {}
86+
return {}
8787

8888
# Error returned
8989
if data.get("finance", {}).get("error", {}):

0 commit comments

Comments
 (0)