Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add extra config to make Unit model hashable #187

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
- add tutorial for protected endpoint with bearer authentication ([#208](https://github.com/RWTH-EBC/FiLiP/issues/208))
- update pandas version to `~=2.1.4` for `python>=3.9` ([#231](https://github.com/RWTH-EBC/FiLiP/pull/231))
- fix: wrong msg in iotac post device ([#214](https://github.com/RWTH-EBC/FiLiP/pull/214))
- fix: make unit model hashable for caching ([#187](https://github.com/RWTH-EBC/FiLiP/issues/187))

#### v0.3.0
- fix: bug in typePattern validation @richardmarston ([#180](https://github.com/RWTH-EBC/FiLiP/pull/180))
Expand Down
9 changes: 7 additions & 2 deletions filip/models/ngsi_v2/units.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ class Unit(BaseModel):
"""
Model for a unit definition
"""
model_config = ConfigDict(extra='ignore', populate_by_name=True)
model_config = ConfigDict(extra='ignore',
populate_by_name=True,
frozen=True)
_ngsi_version: Literal[NgsiVersion.v2] = NgsiVersion.v2
name: Optional[Union[str, UnitText]] = Field(
alias="unitText",
Expand Down Expand Up @@ -206,6 +208,7 @@ def __getattr__(self, item):
Return unit as attribute by name or code.
Notes:
Underscores will be substituted with whitespaces

Args:
item: if len(row) == 0:

Expand All @@ -225,6 +228,7 @@ def quantities(self):
raise NotImplementedError("The used dataset does currently not "
"contain the information about quantity")

@lru_cache()
def __getitem__(self, item: str) -> Unit:
"""
Get unit by name or code
Expand All @@ -236,7 +240,8 @@ def __getitem__(self, item: str) -> Unit:
Unit
"""
idx = self.units.index[((self.units.CommonCode == item.upper()) |
(self.units.Name.str.casefold() == item.casefold()))]
(self.units.Name.str.casefold() ==
item.casefold()))]
if idx.empty:
names = self.units.Name.tolist()
suggestions = [item[0] for item in process.extract(
Expand Down
3 changes: 2 additions & 1 deletion filip/semantics/semantics_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,8 @@ def remove(self, v):
elif isinstance(v, SemanticIndividual):
self._set.remove(v.get_name())
else:
raise KeyError(f"v is neither of type SemanticIndividual nor SemanticClass but {type(v)}")
raise KeyError(f"v is neither of type SemanticIndividual nor "
f"SemanticClass but {type(v)}")

def _add_inverse(self, v: 'SemanticClass'):
"""
Expand Down
56 changes: 54 additions & 2 deletions tests/models/test_units.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""
Test for filip.models.units
"""
from unittest import TestCase
import unittest
import functools
from filip.models.ngsi_v2.units import \
Unit, \
Units, \
Expand All @@ -10,7 +11,7 @@
load_units


class TestUnitCodes(TestCase):
class TestUnitCodes(unittest.TestCase):

def setUp(self):
self.units_data = load_units()
Expand Down Expand Up @@ -39,14 +40,44 @@ def test_unit_text(self):
def test_unit_model(self):
"""
Test unit model

Returns:
None
"""
# test creation
unit = Unit(**self.unit)
json_data = unit.model_dump_json(by_alias=False)
unit_from_json = Unit.model_validate_json(json_data=json_data)
self.assertEqual(unit, unit_from_json)

def test_unit_model_caching(self):
"""
Test caching of unit model

Returns:
None
"""

unit = Unit(**self.unit)
# testing hashing and caching
from functools import lru_cache
from time import perf_counter_ns

self.assertEqual(unit.__hash__(), unit.__hash__())

@functools.lru_cache(maxsize=128)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't that the default anyway? I don't think that python 3.7 should fail because of this. https://docs.python.org/3.7/library/functools.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder about that, but it actually fails with the error message:

TypeError: Expected maxsize to be an integer or None

btw, the python examples all give the maxsize explicitly.

def cache_unit(unit: Unit):
return Unit(name=unit.name)

timers = []
for i in range(5):
start = perf_counter_ns()
cache_unit(unit)
stop = perf_counter_ns()
timers.append(stop - start)
if i > 0:
self.assertLess(timers[i], timers[0])

def test_units(self):
"""
Test units api
Expand Down Expand Up @@ -79,8 +110,29 @@ def test_unit_validator(self):
Returns:
None
"""

unit_data = self.unit.copy()
unit_data['name'] = "celcius"
with self.assertRaises(ValueError):
Unit(**unit_data)

def tearDown(self):
"""
clean up
"""
# using garbage collector to clean up all caches
import gc
gc.collect()

# All objects collected
objects = [i for i in gc.get_objects()
if isinstance(i, functools._lru_cache_wrapper)]

# All objects cleared
for object in objects:
object.cache_clear()

if __name__ == '__main__':
unittest.main()