-
Notifications
You must be signed in to change notification settings - Fork 80
[RFC] Provide an explicit Cache API #891
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
Changes from all commits
f88df97
b380a85
dd1d900
9b47278
820bb39
7e51d93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
from typing import Optional, List, Dict | ||
|
||
from openfisca_core.types import Array | ||
from openfisca_core.periods import Period | ||
|
||
|
||
class Cache: | ||
|
||
|
||
def get(self, variable: str, period: Period) -> Optional[Array]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
""" | ||
Get the cached value of ``variable`` for the given period. | ||
|
||
If the value is not known, return ``None``. | ||
""" | ||
pass | ||
|
||
def put(self, variable: str, period: Period, value: Array) -> None: | ||
""" | ||
Store ``value`` in cache for ``variable`` at period ``period``. | ||
""" | ||
pass | ||
|
||
def delete(self, variable: str, period: Optional[Period] = None) -> None: | ||
""" | ||
If ``period`` is ``None``, remove all known values of the variable. | ||
|
||
If ``period`` is not ``None``, only remove all values for any period included in period (e.g. if period is "2017", values for "2017-01", "2017-07", etc. would be removed) | ||
""" | ||
pass | ||
|
||
def get_known_periods(self, variable: str) -> List[Period]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not convinced this method needs to be part of the public contract. Who needs it? Under what circumstances? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure about the exact circumstances, but it is definitely used in several places in IPP code, when they want to figure out all what's in the cache. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The cache is sometimes updated to init a calculation in a reform. |
||
""" | ||
Get the list of periods the ``variable`` value is known for. | ||
""" | ||
pass | ||
|
||
def get_memory_usage(self) -> Dict[str, Dict]: | ||
""" | ||
Get data about the virtual memory usage of the cache. | ||
""" | ||
pass |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -426,11 +426,29 @@ def set_input(self, variable_name, period, value): | |||||
|
||||||
If a ``set_input`` property has been set for the variable, this method may accept inputs for periods not matching the ``definition_period`` of the variable. To read more about this, check the `documentation <https://openfisca.org/doc/coding-the-legislation/35_periods.html#automatically-process-variable-inputs-defined-for-periods-not-matching-the-definitionperiod>`_. | ||||||
""" | ||||||
variable = self.tax_benefit_system.get_variable(variable_name, check_existence = True) | ||||||
period = periods.period(period) | ||||||
if ((variable.end is not None) and (period.start.date > variable.end)): | ||||||
variable = self.get_variable(variable_name) | ||||||
population = self.get_variable_population(variable_name) | ||||||
|
||||||
# If the variable is constant over time, we ignore the period parameter and set it for ETERNITY | ||||||
if variable.definition_period == periods.ETERNITY: | ||||||
period = periods.ETERNITY_PERIOD | ||||||
if period is not None: | ||||||
period = periods.period(period) | ||||||
|
||||||
if variable.is_inactive(period): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid negation in method name and use |
||||||
return | ||||||
self.get_holder(variable_name).set_input(period, value) | ||||||
array = variable.cast_to_array(value) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
|
||||||
if len(array) != population.count: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
raise ValueError( | ||||||
f'Unable to set value "{value}" for variable "{variable.name}", as its length is {len(array)} while there are {population.count} {population.entity.plural} in the simulation.' | ||||||
) | ||||||
|
||||||
if variable.set_input: | ||||||
return variable.set_input(self, variable, period, array) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
|
||||||
variable.check_input_period(period) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As there is no mention of the input in this method, it seems more general. Rename to |
||||||
self.cache.put_in_cache(variable.name, period, array) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
def get_variable_population(self, variable_name): | ||||||
variable = self.tax_benefit_system.get_variable(variable_name, check_existence = True) | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,40 @@ | ||||||
from pytest import fixture | ||||||
from numpy import asarray as array | ||||||
from numpy.testing import assert_equal | ||||||
|
||||||
from openfisca_core.cache import Cache | ||||||
from openfisca_core import periods | ||||||
|
||||||
period = periods.period(2018) | ||||||
|
||||||
|
||||||
@fixture | ||||||
def cache(): | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I try not to get too hung up on unit/integration test differences, it's a tactical matter rather than one of principle. It's fine if the methods of the object under test rely on some other collaborators, as long as you're primarily testing that object's behaviour, rather than indirectly testing the collaborators. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, thanks a lot for the explanation 🙂 |
||||||
return Cache() | ||||||
|
||||||
|
||||||
def test_does_not_retrieve(cache): | ||||||
value = cache.get_cached_array('toto', period) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
assert value is None | ||||||
|
||||||
|
||||||
def test_retrieve(cache): | ||||||
value_to_cache = array([10, 20]) | ||||||
cache.put_in_cache('toto', period, value_to_cache) | ||||||
value_from_cache = cache.get_cached_array('toto', period) | ||||||
assert_equal(value_to_cache, value_from_cache) | ||||||
|
||||||
|
||||||
def test_delete(cache): | ||||||
value_to_cache = array([10, 20]) | ||||||
cache.put_in_cache('toto', period, value_to_cache) | ||||||
cache.delete_arrays('toto', period) | ||||||
value_from_cache = cache.get_cached_array('toto', period) | ||||||
assert value_from_cache is None | ||||||
|
||||||
|
||||||
def test_retrieve_eternity(cache): | ||||||
value_to_cache = array([10, 20]) | ||||||
cache.put_in_cache('toto', periods.ETERNITY_PERIOD, value_to_cache) | ||||||
value_from_cache = cache.get_cached_array('toto', period) | ||||||
assert_equal(value_to_cache, value_from_cache) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import datetime | ||
|
||
import pytest | ||
import numpy as np | ||
from numpy import asarray as array | ||
from numpy.testing import assert_equal | ||
|
||
from openfisca_core.variables import Variable | ||
from openfisca_core import periods | ||
from openfisca_core.periods import period as make_period | ||
from openfisca_core.errors import PeriodMismatchError | ||
|
||
|
||
class VariableStub(Variable): | ||
|
||
def __init__(self): | ||
pass | ||
|
||
|
||
def test_is_inactive(): | ||
variable = VariableStub() | ||
variable.end = datetime.date(2018, 5, 31) | ||
|
||
assert variable.is_inactive(make_period('2018-06')) | ||
assert not variable.is_inactive(make_period('2018-05')) | ||
|
||
|
||
@pytest.mark.parametrize("definition_period, period", [ | ||
(periods.MONTH, '2019-01'), | ||
(periods.YEAR, '2019'), | ||
(periods.ETERNITY, '2019-01'), | ||
(periods.ETERNITY, '2019'), | ||
(periods.ETERNITY, periods.ETERNITY), | ||
]) | ||
def test_check_input_period_ok(definition_period, period): | ||
variable = VariableStub() | ||
variable.definition_period = definition_period | ||
variable.check_input_period(make_period(period)) | ||
|
||
|
||
@pytest.mark.parametrize("definition_period, period", [ | ||
(periods.MONTH, periods.ETERNITY), | ||
(periods.MONTH, 2018), | ||
(periods.MONTH, "month:2018-01:3"), | ||
(periods.YEAR, periods.ETERNITY), | ||
(periods.YEAR, "2018-01"), | ||
(periods.YEAR, "year:2018:2"), | ||
]) | ||
def test_period_mismatch(definition_period, period): | ||
variable = VariableStub() | ||
variable.definition_period = definition_period | ||
variable.name = 'salary' | ||
|
||
with pytest.raises(PeriodMismatchError): | ||
variable.check_input_period(make_period(period)) | ||
|
||
|
||
@pytest.mark.parametrize("input_value, expected_array", [ | ||
(array([5]), array([5])), | ||
(array(5), array([5])), | ||
([5], array([5])), | ||
(5, array([5])), | ||
('3 + 2', array([5])), | ||
]) | ||
def test_cast_to_float_array(input_value, expected_array): | ||
variable = VariableStub() | ||
variable.value_type = float | ||
variable.dtype = np.float32 | ||
value = variable.cast_to_array(input_value) | ||
assert isinstance(value, np.ndarray) | ||
assert value.ndim == 1 | ||
assert_equal(value, expected_array) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.