Skip to content

Commit 0c36060

Browse files
authored
Merge pull request #2821 from dokson/feature/drop-frozendict-dep
Drop frozendict hard dependency in favour of an internal fallback
2 parents 46ce563 + 593b47c commit 0c36060

5 files changed

Lines changed: 83 additions & 4 deletions

File tree

‎requirements.txt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ requests>=2.31
44
multitasking>=0.0.7
55
platformdirs>=2.0.0
66
pytz>=2022.5
7-
frozendict>=2.3.4
87
beautifulsoup4>=4.11.1
98
lxml>=4.9.0
109
peewee>=3.16.2

‎setup.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
install_requires=['pandas>=1.3.0', 'numpy>=1.16.5',
6363
'requests>=2.31', 'multitasking>=0.0.7',
6464
'platformdirs>=2.0.0', 'pytz>=2022.5',
65-
'frozendict>=2.3.4', 'peewee>=3.16.2',
65+
'peewee>=3.16.2',
6666
'beautifulsoup4>=4.11.1', 'curl_cffi>=0.15',
6767
'protobuf>=3.19.0', 'websockets>=13.0'],
6868
extras_require={

‎tests/test_data.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Tests for yfinance.data internal helpers."""
2+
import unittest
3+
from functools import lru_cache
4+
5+
from yfinance.data import lru_cache_freezeargs
6+
from yfinance.utils import frozendict
7+
8+
9+
class TestFrozenDict(unittest.TestCase):
10+
def test_is_dict_subclass(self):
11+
d = frozendict({"a": 1, "b": 2})
12+
self.assertEqual(d["a"], 1)
13+
self.assertEqual(d.get("b"), 2)
14+
self.assertEqual(set(d.keys()), {"a", "b"})
15+
self.assertIsInstance(d, dict)
16+
17+
def test_is_hashable(self):
18+
d1 = frozendict({"a": 1, "b": 2})
19+
d2 = frozendict({"b": 2, "a": 1})
20+
self.assertEqual(hash(d1), hash(d2))
21+
self.assertEqual(len({d1, d2}), 1)
22+
23+
def test_mutation_raises(self):
24+
d = frozendict({"a": 1})
25+
with self.assertRaises(TypeError):
26+
d["b"] = 2
27+
with self.assertRaises(TypeError):
28+
del d["a"]
29+
with self.assertRaises(AttributeError):
30+
d.pop("a")
31+
with self.assertRaises(AttributeError):
32+
d.clear()
33+
with self.assertRaises(AttributeError):
34+
d.update({"x": 9})
35+
36+
def test_lru_cache_integration(self):
37+
call_count = {"n": 0}
38+
39+
@lru_cache_freezeargs
40+
@lru_cache(maxsize=8)
41+
def fn(params):
42+
call_count["n"] += 1
43+
return sum(params.values())
44+
45+
self.assertEqual(fn({"a": 1, "b": 2}), 3)
46+
self.assertEqual(fn({"a": 1, "b": 2}), 3)
47+
self.assertEqual(call_count["n"], 1)
48+
self.assertEqual(fn({"a": 1, "b": 3}), 4)
49+
self.assertEqual(call_count["n"], 2)
50+
51+
52+
if __name__ == "__main__":
53+
unittest.main()

‎yfinance/data.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99
from bs4 import BeautifulSoup
1010
import datetime
1111

12-
from frozendict import frozendict
13-
1412
from . import utils, cache
13+
from .utils import frozendict
1514
from .config import YfConfig
1615
import threading
1716

‎yfinance/utils.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,34 @@
4343
from yfinance.exceptions import YFException
4444
from yfinance.config import YfConfig
4545

46+
# Use the third-party ``frozendict`` package if installed; otherwise fall
47+
# back to a small pure-Python equivalent (PEP 814).
48+
try:
49+
from frozendict import frozendict # type: ignore[import-not-found]
50+
except ImportError:
51+
class frozendict(dict): # type: ignore[no-redef]
52+
"""Hashable, read-only ``dict`` used as an ``lru_cache`` key."""
53+
__slots__ = ()
54+
55+
def __hash__(self): # type: ignore[override]
56+
return hash(frozenset(self.items()))
57+
58+
def __setitem__(self, *args, **kwargs):
59+
raise TypeError(f"'{type(self).__name__}' object doesn't support item assignment")
60+
61+
def __delitem__(self, *args, **kwargs):
62+
raise TypeError(f"'{type(self).__name__}' object doesn't support item deletion")
63+
64+
def _readonly(self, *args, **kwargs):
65+
raise AttributeError(f"'{type(self).__name__}' object is read-only")
66+
67+
pop = _readonly # type: ignore[assignment]
68+
popitem = _readonly # type: ignore[assignment]
69+
clear = _readonly # type: ignore[assignment]
70+
update = _readonly # type: ignore[assignment]
71+
setdefault = _readonly # type: ignore[assignment]
72+
73+
4674
# From https://stackoverflow.com/a/59128615
4775
def attributes(obj):
4876
disallowed_names = {

0 commit comments

Comments
 (0)