diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index b3b5d8cbb2..7c5c89e3e5 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -4,6 +4,7 @@ to OpenFisca Core and to country packages. Official Public API: + * :class:`.either` * :func:`.apply_thresholds` * :func:`.average_rate` * :func:`.concat` @@ -50,18 +51,16 @@ """ -# Official Public API +from ._adts import Either, Failure, Success +from .dummy import Dummy # Deprecated +from .formulas import apply_thresholds, concat, switch +from .misc import empty_clone, stringify_array +from .rates import average_rate, marginal_rate -from .formulas import apply_thresholds, concat, switch # noqa: F401 -from .misc import empty_clone, stringify_array # noqa: F401 -from .rates import average_rate, marginal_rate # noqa: F401 +either = Either __all__ = ["apply_thresholds", "concat", "switch"] __all__ = ["empty_clone", "stringify_array", *__all__] __all__ = ["average_rate", "marginal_rate", *__all__] - -# Deprecated - -from .dummy import Dummy # noqa: F401 - +__all__ = ["Either", "Failure", "Success", "either", *__all__] __all__ = ["Dummy", *__all__] diff --git a/openfisca_core/commons/_adts.py b/openfisca_core/commons/_adts.py new file mode 100644 index 0000000000..4b7c073026 --- /dev/null +++ b/openfisca_core/commons/_adts.py @@ -0,0 +1,116 @@ +"""Algebraic data types for OpenFisca. + +An algebraic data type is a structured type that’s formed by composing other +types. [...] Product types allow you to have more than one value in a single +structure, at the same time. [...] Sum types are types where your value must +be one of a fixed set of options. + +.. _See: + https://jrsinclair.com/articles/2019/algebraic-data-types-what-i-wish-someone-had-explained-about-functional-programming/ + +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Generic, TypeVar, cast, final +from typing_extensions import Never + +import dataclasses + +#: Type variable representing an error. +E = TypeVar("E") + +#: Type variable representing a value. +A = TypeVar("A") + + +@dataclasses.dataclass(frozen=True) +class Either(Generic[E, A]): + """The Either monad. + + The Either monad specifies the Either data type as well as several + functions that operate on top of it. The Either data type represents the + result of a computation that may fail. + + """ + + #: The value or state passed on. + _value: E | A + + @property + @final + def is_failure(self) -> bool: + """bool: Whether this instance represents a failure.""" + return isinstance(self, Failure) + + @property + @final + def is_success(self) -> bool: + """bool: Whether this instance represents a success.""" + return isinstance(self, Success) + + @final + def unwrap(self) -> E | A: + """Return the value of this instance. + + Examples: + >>> Either.fail("error").unwrap() + 'error' + + >>> Either.succeed(1).unwrap() + 1 + + Returns: + E | A: The value of this instance. + + """ + + return self._value + + @final + def then( + self, f: Callable[[A], Failure[E] | Success[A]] + ) -> Failure[E] | Success[A]: + """Apply a flatMap to input stream. + + Examples: + >>> Either.fail("error").then(lambda x: Either.succeed(x)).unwrap() + 'error' + + >>> Either.succeed(1).then(lambda x: Either.succeed(x + 1)).unwrap() + 2 + + Args: + f: A function that takes a value and returns a new Either instance. + + Returns: + _Failure[E] | _Success[A]: The result of applying f. + + """ + + if self.is_success: + return f(cast(A, self.unwrap())) + return Either.fail(cast(E, self.unwrap())) + + @staticmethod + @final + def fail(value: E) -> Failure[E]: + """_Failure[E]: Create a failing result.""" + return Failure(value) + + @staticmethod + @final + def succeed(value: A) -> Success[A]: + """_Success[A]: Create a successful result.""" + return Success(value) + + +@final +class Failure(Either[E, Never]): + """A failing result in an Either ADT.""" + + +@final +class Success(Either[Never, A]): + """A successful result in an Either ADT.""" diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index bbcc4fe565..1cb35a571e 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,17 +1,15 @@ -from typing import Any, Dict, Sequence, TypeVar +from typing import Any, Dict, Union from openfisca_core.types import Array, ArrayLike import numpy -T = TypeVar("T") - def apply_thresholds( - input: Array[float], + input: Array[numpy.float_], thresholds: ArrayLike[float], choices: ArrayLike[float], -) -> Array[float]: +) -> Array[numpy.float_]: """Makes a choice based on an input and thresholds. From a list of ``choices``, this function selects one of these values @@ -40,7 +38,7 @@ def apply_thresholds( """ - condlist: Sequence[Array[bool]] + condlist: list[Union[Array[numpy.bool_], bool]] condlist = [input <= threshold for threshold in thresholds] if len(condlist) == len(choices) - 1: @@ -58,7 +56,9 @@ def apply_thresholds( return numpy.select(condlist, choices) -def concat(this: ArrayLike[str], that: ArrayLike[str]) -> Array[str]: +def concat( + this: Union[Array[Any], ArrayLike[str]], that: Union[Array[Any], ArrayLike[str]] +) -> Array[numpy.str_]: """Concatenates the values of two arrays. Args: @@ -88,8 +88,8 @@ def concat(this: ArrayLike[str], that: ArrayLike[str]) -> Array[str]: def switch( conditions: Array[Any], - value_by_condition: Dict[float, T], -) -> Array[T]: + value_by_condition: Dict[float, Any], +) -> Array[Any]: """Mimicks a switch statement. Given an array of conditions, returns an array of the same size, @@ -120,4 +120,4 @@ def switch( condlist = [conditions == condition for condition in value_by_condition.keys()] - return numpy.select(condlist, value_by_condition.values()) + return numpy.select(condlist, tuple(value_by_condition.values())) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index ee985071bf..9c7fcc68fd 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,4 +1,4 @@ -from typing import TypeVar +from typing import Any, TypeVar, Union from openfisca_core.types import Array @@ -43,7 +43,7 @@ def empty_clone(original: T) -> T: return new -def stringify_array(array: Array) -> str: +def stringify_array(array: Union[Array[Any], None]) -> str: """Generates a clean string representation of a numpy array. Args: diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index 7ede496f8c..6df1f1fee2 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -6,10 +6,10 @@ def average_rate( - target: Array[float], + target: Array[numpy.float_], varying: ArrayLike[float], trim: Optional[ArrayLike[float]] = None, -) -> Array[float]: +) -> Array[numpy.float_]: """Computes the average rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -41,7 +41,7 @@ def average_rate( """ - average_rate: Array[float] + average_rate: Array[numpy.float_] average_rate = 1 - target / varying @@ -62,10 +62,10 @@ def average_rate( def marginal_rate( - target: Array[float], - varying: Array[float], + target: Array[numpy.float_], + varying: Array[numpy.float_], trim: Optional[ArrayLike[float]] = None, -) -> Array[float]: +) -> Array[numpy.float_]: """Computes the marginal rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -97,7 +97,7 @@ def marginal_rate( """ - marginal_rate: Array[float] + marginal_rate: Array[numpy.float_] marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) diff --git a/openfisca_core/commons/tests/test_adts.py b/openfisca_core/commons/tests/test_adts.py new file mode 100644 index 0000000000..9ff64d778c --- /dev/null +++ b/openfisca_core/commons/tests/test_adts.py @@ -0,0 +1,33 @@ +import pytest + +from openfisca_core import commons + + +@pytest.fixture +def failure(): + return commons.either.fail("error") + + +@pytest.fixture +def success(): + return commons.either.succeed(1) + + +def test_either_is_failure(failure): + assert failure.is_failure + + +def test_either_is_success(success): + assert success.is_success + + +def test_either_unwrap(failure): + assert failure.unwrap() == "error" + + +def test_either_then(failure, success): + assert failure.then(lambda x: failure).unwrap() == "error" + assert failure.then(lambda x: success).unwrap() == "error" + assert success.then(lambda x: failure).unwrap() == "error" + assert success.then(lambda x: success).unwrap() == 1 + assert success.then(lambda x: commons.either.succeed(x + 1)).unwrap() == 2 diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py index 8fb472046b..c8ab3be3ee 100644 --- a/openfisca_core/data_storage/in_memory_storage.py +++ b/openfisca_core/data_storage/in_memory_storage.py @@ -1,7 +1,6 @@ import numpy from openfisca_core import periods -from openfisca_core.periods import DateUnit class InMemoryStorage: @@ -15,7 +14,7 @@ def __init__(self, is_eternal=False): def get(self, period): if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) + period = periods.period(periods.ETERNITY) period = periods.period(period) values = self._arrays.get(period) @@ -25,7 +24,7 @@ def get(self, period): def put(self, value, period): if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) + period = periods.period(periods.ETERNITY) period = periods.period(period) self._arrays[period] = value @@ -36,7 +35,7 @@ def delete(self, period=None): return if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) + period = periods.period(periods.ETERNITY) period = periods.period(period) self._arrays = { diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py index dbf8a4eb13..f481dfd7bc 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -5,7 +5,6 @@ from openfisca_core import periods from openfisca_core.indexed_enums import EnumArray -from openfisca_core.periods import DateUnit class OnDiskStorage: @@ -29,7 +28,7 @@ def _decode_file(self, file): def get(self, period): if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) + period = periods.period(periods.ETERNITY) period = periods.period(period) values = self._files.get(period) @@ -39,7 +38,7 @@ def get(self, period): def put(self, value, period): if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) + period = periods.period(periods.ETERNITY) period = periods.period(period) filename = str(period) @@ -56,7 +55,7 @@ def delete(self, period=None): return if self.is_eternal: - period = periods.period(DateUnit.ETERNITY) + period = periods.period(periods.ETERNITY) period = periods.period(period) if period is not None: diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index a1cd397a3a..9546773cb8 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -27,4 +27,14 @@ from .helpers import build_entity, find_role from .role import Role -__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "find_role", "types"] +SingleEntity = Entity + +__all__ = [ + "Entity", + "SingleEntity", + "GroupEntity", + "Role", + "build_entity", + "find_role", + "types", +] diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index ecd17becae..ea0cd8c589 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -49,6 +49,10 @@ def get_variable( check_existence: bool = False, ) -> Variable | None: """Get a ``variable_name`` from ``variables``.""" + if self._tax_benefit_system is None: + raise ValueError( + "You must set 'tax_benefit_system' before calling this method." + ) return self._tax_benefit_system.get_variable(variable_name, check_existence) def check_variable_defined_for_entity(self, variable_name: str) -> None: diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index 1a2fb06b2c..f1e580fa2a 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -5,7 +5,8 @@ class Entity(Protocol): - ... + key: str + plural: str | None class GroupEntity(Protocol): diff --git a/openfisca_core/holders/helpers.py b/openfisca_core/holders/helpers.py index 0e88964fc7..aba3a054a8 100644 --- a/openfisca_core/holders/helpers.py +++ b/openfisca_core/holders/helpers.py @@ -21,7 +21,7 @@ def set_input_dispatch_by_period(holder, period, array): period_unit = period.unit if holder.variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar + periods.ISOFORMAT + periods.ISOCALENDAR ): raise ValueError( "set_input_dispatch_by_period can't be used for eternal variables." @@ -57,7 +57,7 @@ def set_input_divide_by_period(holder, period, array): period_unit = period.unit if holder.variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar + periods.ISOFORMAT + periods.ISOCALENDAR ): raise ValueError( "set_input_divide_by_period can't be used for eternal variables." diff --git a/openfisca_core/holders/tests/test_helpers.py b/openfisca_core/holders/tests/test_helpers.py index d76040d676..f9e6b9062a 100644 --- a/openfisca_core/holders/tests/test_helpers.py +++ b/openfisca_core/holders/tests/test_helpers.py @@ -1,9 +1,8 @@ import pytest -from openfisca_core import holders, tools +from openfisca_core import holders, periods, tools from openfisca_core.entities import Entity from openfisca_core.holders import Holder -from openfisca_core.periods import DateUnit, Instant, Period from openfisca_core.populations import Population from openfisca_core.variables import Variable @@ -37,36 +36,36 @@ def population(people): @pytest.mark.parametrize( "dispatch_unit, definition_unit, values, expected", [ - [DateUnit.YEAR, DateUnit.YEAR, [1.0], [3.0]], - [DateUnit.YEAR, DateUnit.MONTH, [1.0], [36.0]], - [DateUnit.YEAR, DateUnit.DAY, [1.0], [1096.0]], - [DateUnit.YEAR, DateUnit.WEEK, [1.0], [157.0]], - [DateUnit.YEAR, DateUnit.WEEKDAY, [1.0], [1096.0]], - [DateUnit.MONTH, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.MONTH, DateUnit.MONTH, [1.0], [3.0]], - [DateUnit.MONTH, DateUnit.DAY, [1.0], [90.0]], - [DateUnit.MONTH, DateUnit.WEEK, [1.0], [13.0]], - [DateUnit.MONTH, DateUnit.WEEKDAY, [1.0], [90.0]], - [DateUnit.DAY, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.DAY, [1.0], [3.0]], - [DateUnit.DAY, DateUnit.WEEK, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.WEEKDAY, [1.0], [3.0]], - [DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.DAY, [1.0], [21.0]], - [DateUnit.WEEK, DateUnit.WEEK, [1.0], [3.0]], - [DateUnit.WEEK, DateUnit.WEEKDAY, [1.0], [21.0]], - [DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.DAY, [1.0], [21.0]], - [DateUnit.WEEK, DateUnit.WEEK, [1.0], [3.0]], - [DateUnit.WEEK, DateUnit.WEEKDAY, [1.0], [21.0]], - [DateUnit.WEEKDAY, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.DAY, [1.0], [3.0]], - [DateUnit.WEEKDAY, DateUnit.WEEK, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.WEEKDAY, [1.0], [3.0]], + [periods.YEAR, periods.YEAR, [1.0], [3.0]], + [periods.YEAR, periods.MONTH, [1.0], [36.0]], + [periods.YEAR, periods.DAY, [1.0], [1096.0]], + [periods.YEAR, periods.WEEK, [1.0], [157.0]], + [periods.YEAR, periods.WEEKDAY, [1.0], [1096.0]], + [periods.MONTH, periods.YEAR, [1.0], [1.0]], + [periods.MONTH, periods.MONTH, [1.0], [3.0]], + [periods.MONTH, periods.DAY, [1.0], [90.0]], + [periods.MONTH, periods.WEEK, [1.0], [13.0]], + [periods.MONTH, periods.WEEKDAY, [1.0], [90.0]], + [periods.DAY, periods.YEAR, [1.0], [1.0]], + [periods.DAY, periods.MONTH, [1.0], [1.0]], + [periods.DAY, periods.DAY, [1.0], [3.0]], + [periods.DAY, periods.WEEK, [1.0], [1.0]], + [periods.DAY, periods.WEEKDAY, [1.0], [3.0]], + [periods.WEEK, periods.YEAR, [1.0], [1.0]], + [periods.WEEK, periods.MONTH, [1.0], [1.0]], + [periods.WEEK, periods.DAY, [1.0], [21.0]], + [periods.WEEK, periods.WEEK, [1.0], [3.0]], + [periods.WEEK, periods.WEEKDAY, [1.0], [21.0]], + [periods.WEEK, periods.YEAR, [1.0], [1.0]], + [periods.WEEK, periods.MONTH, [1.0], [1.0]], + [periods.WEEK, periods.DAY, [1.0], [21.0]], + [periods.WEEK, periods.WEEK, [1.0], [3.0]], + [periods.WEEK, periods.WEEKDAY, [1.0], [21.0]], + [periods.WEEKDAY, periods.YEAR, [1.0], [1.0]], + [periods.WEEKDAY, periods.MONTH, [1.0], [1.0]], + [periods.WEEKDAY, periods.DAY, [1.0], [3.0]], + [periods.WEEKDAY, periods.WEEK, [1.0], [1.0]], + [periods.WEEKDAY, periods.WEEKDAY, [1.0], [3.0]], ], ) def test_set_input_dispatch_by_period( @@ -80,8 +79,8 @@ def test_set_input_dispatch_by_period( Income.definition_period = definition_unit income = Income() holder = Holder(income, population) - instant = Instant((2022, 1, 1)) - dispatch_period = Period((dispatch_unit, instant, 3)) + instant = periods.Instant((2022, 1, 1)) + dispatch_period = periods.Period((dispatch_unit, instant, 3)) holders.set_input_dispatch_by_period(holder, dispatch_period, values) total = sum(map(holder.get_array, holder.get_known_periods())) @@ -92,31 +91,31 @@ def test_set_input_dispatch_by_period( @pytest.mark.parametrize( "divide_unit, definition_unit, values, expected", [ - [DateUnit.YEAR, DateUnit.YEAR, [3.0], [1.0]], - [DateUnit.YEAR, DateUnit.MONTH, [36.0], [1.0]], - [DateUnit.YEAR, DateUnit.DAY, [1095.0], [1.0]], - [DateUnit.YEAR, DateUnit.WEEK, [157.0], [1.0]], - [DateUnit.YEAR, DateUnit.WEEKDAY, [1095.0], [1.0]], - [DateUnit.MONTH, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.MONTH, DateUnit.MONTH, [3.0], [1.0]], - [DateUnit.MONTH, DateUnit.DAY, [90.0], [1.0]], - [DateUnit.MONTH, DateUnit.WEEK, [13.0], [1.0]], - [DateUnit.MONTH, DateUnit.WEEKDAY, [90.0], [1.0]], - [DateUnit.DAY, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.DAY, [3.0], [1.0]], - [DateUnit.DAY, DateUnit.WEEK, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.WEEKDAY, [3.0], [1.0]], - [DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.DAY, [21.0], [1.0]], - [DateUnit.WEEK, DateUnit.WEEK, [3.0], [1.0]], - [DateUnit.WEEK, DateUnit.WEEKDAY, [21.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.DAY, [3.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.WEEK, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.WEEKDAY, [3.0], [1.0]], + [periods.YEAR, periods.YEAR, [3.0], [1.0]], + [periods.YEAR, periods.MONTH, [36.0], [1.0]], + [periods.YEAR, periods.DAY, [1095.0], [1.0]], + [periods.YEAR, periods.WEEK, [157.0], [1.0]], + [periods.YEAR, periods.WEEKDAY, [1095.0], [1.0]], + [periods.MONTH, periods.YEAR, [1.0], [1.0]], + [periods.MONTH, periods.MONTH, [3.0], [1.0]], + [periods.MONTH, periods.DAY, [90.0], [1.0]], + [periods.MONTH, periods.WEEK, [13.0], [1.0]], + [periods.MONTH, periods.WEEKDAY, [90.0], [1.0]], + [periods.DAY, periods.YEAR, [1.0], [1.0]], + [periods.DAY, periods.MONTH, [1.0], [1.0]], + [periods.DAY, periods.DAY, [3.0], [1.0]], + [periods.DAY, periods.WEEK, [1.0], [1.0]], + [periods.DAY, periods.WEEKDAY, [3.0], [1.0]], + [periods.WEEK, periods.YEAR, [1.0], [1.0]], + [periods.WEEK, periods.MONTH, [1.0], [1.0]], + [periods.WEEK, periods.DAY, [21.0], [1.0]], + [periods.WEEK, periods.WEEK, [3.0], [1.0]], + [periods.WEEK, periods.WEEKDAY, [21.0], [1.0]], + [periods.WEEKDAY, periods.YEAR, [1.0], [1.0]], + [periods.WEEKDAY, periods.MONTH, [1.0], [1.0]], + [periods.WEEKDAY, periods.DAY, [3.0], [1.0]], + [periods.WEEKDAY, periods.WEEK, [1.0], [1.0]], + [periods.WEEKDAY, periods.WEEKDAY, [3.0], [1.0]], ], ) def test_set_input_divide_by_period( @@ -130,8 +129,8 @@ def test_set_input_divide_by_period( Income.definition_period = definition_unit income = Income() holder = Holder(income, population) - instant = Instant((2022, 1, 1)) - divide_period = Period((divide_unit, instant, 3)) + instant = periods.Instant((2022, 1, 1)) + divide_period = periods.Period((divide_unit, instant, 3)) holders.set_input_divide_by_period(holder, divide_period, values) last = holder.get_array(holder.get_known_periods()[-1]) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 4669c7ff4f..ca23dbf765 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -21,20 +21,14 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .config import ( # noqa: F401 - DAY, - ETERNITY, +from .config import ( INSTANT_PATTERN, - MONTH, - WEEK, - WEEKDAY, - YEAR, date_by_instant_cache, str_by_instant_cache, year_or_month_or_day_re, ) -from .date_unit import DateUnit # noqa: F401 -from .helpers import ( # noqa: F401 +from .date_unit import DateUnit +from .helpers import ( instant, instant_date, key_period_size, @@ -42,5 +36,38 @@ unit_weight, unit_weights, ) -from .instant_ import Instant # noqa: F401 -from .period_ import Period # noqa: F401 +from .instant_ import Instant +from .period_ import Period + +WEEKDAY = DateUnit.WEEKDAY +WEEK = DateUnit.WEEK +DAY = DateUnit.DAY +MONTH = DateUnit.MONTH +YEAR = DateUnit.YEAR +ETERNITY = DateUnit.ETERNITY +ISOFORMAT = DateUnit.isoformat +ISOCALENDAR = DateUnit.isocalendar + +__all__ = [ + "INSTANT_PATTERN", + "date_by_instant_cache", + "str_by_instant_cache", + "year_or_month_or_day_re", + "DateUnit", + "instant", + "instant_date", + "key_period_size", + "period", + "unit_weight", + "unit_weights", + "Instant", + "Period", + "WEEKDAY", + "WEEK", + "DAY", + "MONTH", + "YEAR", + "ETERNITY", + "ISOFORMAT", + "ISOCALENDAR", +] diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/config.py index 17807160e4..afbfbd9d0d 100644 --- a/openfisca_core/periods/config.py +++ b/openfisca_core/periods/config.py @@ -1,14 +1,5 @@ import re -from .date_unit import DateUnit - -WEEKDAY = DateUnit.WEEKDAY -WEEK = DateUnit.WEEK -DAY = DateUnit.DAY -MONTH = DateUnit.MONTH -YEAR = DateUnit.YEAR -ETERNITY = DateUnit.ETERNITY - # Matches "2015", "2015-01", "2015-01-01" # Does not match "2015-13", "2015-12-32" INSTANT_PATTERN = re.compile( diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 11a7b671b4..211b7387ca 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -269,14 +269,14 @@ def size_in_years(self) -> int: >>> period = Period((DateUnit.MONTH, instant, 3)) >>> period.size_in_years Traceback (most recent call last): - ValueError: Can't calculate number of years in a month. + ValueError: Cannot calculate number of years in month. """ if self.unit == DateUnit.YEAR: return self.size - raise ValueError(f"Can't calculate number of years in a {self.unit}.") + raise ValueError(f"Cannot calculate number of years in {self.unit}.") @property def size_in_months(self) -> int: @@ -292,7 +292,7 @@ def size_in_months(self) -> int: >>> period = Period((DateUnit.DAY, instant, 3)) >>> period.size_in_months Traceback (most recent call last): - ValueError: Can't calculate number of months in a day. + ValueError: Cannot calculate number of months in day. """ @@ -302,7 +302,7 @@ def size_in_months(self) -> int: if self.unit == DateUnit.MONTH: return self.size - raise ValueError(f"Can't calculate number of months in a {self.unit}.") + raise ValueError(f"Cannot calculate number of months in {self.unit}.") @property def size_in_days(self) -> int: @@ -331,7 +331,7 @@ def size_in_days(self) -> int: if self.unit in (DateUnit.DAY, DateUnit.WEEKDAY): return self.size - raise ValueError(f"Can't calculate number of days in a {self.unit}.") + raise ValueError(f"Cannot calculate number of days in {self.unit}.") @property def size_in_weeks(self): @@ -365,7 +365,7 @@ def size_in_weeks(self): if self.unit == DateUnit.WEEK: return self.size - raise ValueError(f"Can't calculate number of weeks in a {self.unit}.") + raise ValueError(f"Cannot calculate number of weeks in {self.unit}.") @property def size_in_weekdays(self): @@ -397,7 +397,7 @@ def size_in_weekdays(self): if self.unit in (DateUnit.DAY, DateUnit.WEEKDAY): return self.size - raise ValueError(f"Can't calculate number of weekdays in a {self.unit}.") + raise ValueError(f"Cannot calculate number of weekdays in {self.unit}.") @property def days(self): diff --git a/openfisca_core/periods/tests/helpers/test_instant.py b/openfisca_core/periods/tests/helpers/test_instant.py index cb74c55ca4..c1bd389d0d 100644 --- a/openfisca_core/periods/tests/helpers/test_instant.py +++ b/openfisca_core/periods/tests/helpers/test_instant.py @@ -12,7 +12,10 @@ [None, None], [datetime.date(1, 1, 1), Instant((1, 1, 1))], [Instant((1, 1, 1)), Instant((1, 1, 1))], - [Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))], + [ + Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), + Instant((1, 1, 1)), + ], [-1, Instant((-1, 1, 1))], [0, Instant((0, 1, 1))], [1, Instant((1, 1, 1))], @@ -25,7 +28,10 @@ [(None, None), Instant((None, None, 1))], [(None, None, None), Instant((None, None, None))], [(datetime.date(1, 1, 1),), Instant((datetime.date(1, 1, 1), 1, 1))], - [(Instant((1, 1, 1)),), Instant((Instant((1, 1, 1)), 1, 1))], + [ + (Instant((1, 1, 1)),), + Instant((Instant((1, 1, 1)), 1, 1)), + ], [ (Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), Instant((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), 1, 1)), diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py index 7d50abe102..7751fd23ce 100644 --- a/openfisca_core/periods/tests/helpers/test_period.py +++ b/openfisca_core/periods/tests/helpers/test_period.py @@ -9,14 +9,26 @@ @pytest.mark.parametrize( "arg, expected", [ - ["eternity", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf")))], - ["ETERNITY", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf")))], + [ + "eternity", + Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf"))), + ], + [ + "ETERNITY", + Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf"))), + ], [ DateUnit.ETERNITY, Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf"))), ], - [datetime.date(1, 1, 1), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))], - [Instant((1, 1, 1)), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))], + [ + datetime.date(1, 1, 1), + Period((DateUnit.DAY, Instant((1, 1, 1)), 1)), + ], + [ + Instant((1, 1, 1)), + Period((DateUnit.DAY, Instant((1, 1, 1)), 1)), + ], [ Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), @@ -29,37 +41,127 @@ ["1001", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], ["1001-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], ["1001-01-01", Period((DateUnit.DAY, Instant((1001, 1, 1)), 1))], - ["1004-02-29", Period((DateUnit.DAY, Instant((1004, 2, 29)), 1))], - ["1001-W01", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))], + [ + "1004-02-29", + Period((DateUnit.DAY, Instant((1004, 2, 29)), 1)), + ], + [ + "1001-W01", + Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1)), + ], + [ + "1001-W01-1", + Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1)), + ], ["year:1001", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-01", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-01-01", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-W01", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))], - ["year:1001-W01-1", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))], - ["year:1001:1", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-01:1", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-01-01:1", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-W01:1", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))], - ["year:1001-W01-1:1", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))], - ["year:1001:3", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3))], - ["year:1001-01:3", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3))], - ["year:1001-01-01:3", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3))], - ["year:1001-W01:3", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 3))], - ["year:1001-W01-1:3", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 3))], - ["month:1001-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], - ["month:1001-01-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], - ["week:1001-W01", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["week:1001-W01-1", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["month:1001-01:1", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], - ["month:1001-01:3", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 3))], - ["month:1001-01-01:3", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 3))], - ["week:1001-W01:1", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["week:1001-W01:3", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 3))], - ["week:1001-W01-1:3", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 3))], - ["day:1001-01-01", Period((DateUnit.DAY, Instant((1001, 1, 1)), 1))], - ["day:1001-01-01:3", Period((DateUnit.DAY, Instant((1001, 1, 1)), 3))], - ["weekday:1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))], + [ + "year:1001-01", + Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1)), + ], + [ + "year:1001-01-01", + Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1)), + ], + [ + "year:1001-W01", + Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1)), + ], + [ + "year:1001-W01-1", + Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1)), + ], + [ + "year:1001:1", + Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1)), + ], + [ + "year:1001-01:1", + Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1)), + ], + [ + "year:1001-01-01:1", + Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1)), + ], + [ + "year:1001-W01:1", + Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1)), + ], + [ + "year:1001-W01-1:1", + Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1)), + ], + [ + "year:1001:3", + Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3)), + ], + [ + "year:1001-01:3", + Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3)), + ], + [ + "year:1001-01-01:3", + Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3)), + ], + [ + "year:1001-W01:3", + Period((DateUnit.YEAR, Instant((1000, 12, 29)), 3)), + ], + [ + "year:1001-W01-1:3", + Period((DateUnit.YEAR, Instant((1000, 12, 29)), 3)), + ], + [ + "month:1001-01", + Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1)), + ], + [ + "month:1001-01-01", + Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1)), + ], + [ + "week:1001-W01", + Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1)), + ], + [ + "week:1001-W01-1", + Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1)), + ], + [ + "month:1001-01:1", + Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1)), + ], + [ + "month:1001-01:3", + Period((DateUnit.MONTH, Instant((1001, 1, 1)), 3)), + ], + [ + "month:1001-01-01:3", + Period((DateUnit.MONTH, Instant((1001, 1, 1)), 3)), + ], + [ + "week:1001-W01:1", + Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1)), + ], + [ + "week:1001-W01:3", + Period((DateUnit.WEEK, Instant((1000, 12, 29)), 3)), + ], + [ + "week:1001-W01-1:3", + Period((DateUnit.WEEK, Instant((1000, 12, 29)), 3)), + ], + [ + "day:1001-01-01", + Period((DateUnit.DAY, Instant((1001, 1, 1)), 1)), + ], + [ + "day:1001-01-01:3", + Period((DateUnit.DAY, Instant((1001, 1, 1)), 3)), + ], + [ + "weekday:1001-W01-1", + Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1)), + ], [ "weekday:1001-W01-1:3", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 3)), diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test__parsers.py index 6c88c9cd11..a78a119421 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test__parsers.py @@ -11,9 +11,18 @@ ["1001-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], ["1001-12", Period((DateUnit.MONTH, Instant((1001, 12, 1)), 1))], ["1001-01-01", Period((DateUnit.DAY, Instant((1001, 1, 1)), 1))], - ["1001-W01", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["1001-W52", Period((DateUnit.WEEK, Instant((1001, 12, 21)), 1))], - ["1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))], + [ + "1001-W01", + Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1)), + ], + [ + "1001-W52", + Period((DateUnit.WEEK, Instant((1001, 12, 21)), 1)), + ], + [ + "1001-W01-1", + Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1)), + ], ], ) def test__parse_period(arg, expected): diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 21549008f4..6c04f78f23 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -6,26 +6,101 @@ @pytest.mark.parametrize( "instant, offset, unit, expected", [ - [Instant((2020, 2, 29)), "first-of", DateUnit.YEAR, Instant((2020, 1, 1))], - [Instant((2020, 2, 29)), "first-of", DateUnit.MONTH, Instant((2020, 2, 1))], - [Instant((2020, 2, 29)), "first-of", DateUnit.WEEK, Instant((2020, 2, 24))], + [ + Instant((2020, 2, 29)), + "first-of", + DateUnit.YEAR, + Instant((2020, 1, 1)), + ], + [ + Instant((2020, 2, 29)), + "first-of", + DateUnit.MONTH, + Instant((2020, 2, 1)), + ], + [ + Instant((2020, 2, 29)), + "first-of", + DateUnit.WEEK, + Instant((2020, 2, 24)), + ], [Instant((2020, 2, 29)), "first-of", DateUnit.DAY, None], [Instant((2020, 2, 29)), "first-of", DateUnit.WEEKDAY, None], - [Instant((2020, 2, 29)), "last-of", DateUnit.YEAR, Instant((2020, 12, 31))], - [Instant((2020, 2, 29)), "last-of", DateUnit.MONTH, Instant((2020, 2, 29))], - [Instant((2020, 2, 29)), "last-of", DateUnit.WEEK, Instant((2020, 3, 1))], + [ + Instant((2020, 2, 29)), + "last-of", + DateUnit.YEAR, + Instant((2020, 12, 31)), + ], + [ + Instant((2020, 2, 29)), + "last-of", + DateUnit.MONTH, + Instant((2020, 2, 29)), + ], + [ + Instant((2020, 2, 29)), + "last-of", + DateUnit.WEEK, + Instant((2020, 3, 1)), + ], [Instant((2020, 2, 29)), "last-of", DateUnit.DAY, None], [Instant((2020, 2, 29)), "last-of", DateUnit.WEEKDAY, None], - [Instant((2020, 2, 29)), -3, DateUnit.YEAR, Instant((2017, 2, 28))], - [Instant((2020, 2, 29)), -3, DateUnit.MONTH, Instant((2019, 11, 29))], - [Instant((2020, 2, 29)), -3, DateUnit.WEEK, Instant((2020, 2, 8))], - [Instant((2020, 2, 29)), -3, DateUnit.DAY, Instant((2020, 2, 26))], - [Instant((2020, 2, 29)), -3, DateUnit.WEEKDAY, Instant((2020, 2, 26))], - [Instant((2020, 2, 29)), 3, DateUnit.YEAR, Instant((2023, 2, 28))], - [Instant((2020, 2, 29)), 3, DateUnit.MONTH, Instant((2020, 5, 29))], - [Instant((2020, 2, 29)), 3, DateUnit.WEEK, Instant((2020, 3, 21))], + [ + Instant((2020, 2, 29)), + -3, + DateUnit.YEAR, + Instant((2017, 2, 28)), + ], + [ + Instant((2020, 2, 29)), + -3, + DateUnit.MONTH, + Instant((2019, 11, 29)), + ], + [ + Instant((2020, 2, 29)), + -3, + DateUnit.WEEK, + Instant((2020, 2, 8)), + ], + [ + Instant((2020, 2, 29)), + -3, + DateUnit.DAY, + Instant((2020, 2, 26)), + ], + [ + Instant((2020, 2, 29)), + -3, + DateUnit.WEEKDAY, + Instant((2020, 2, 26)), + ], + [ + Instant((2020, 2, 29)), + 3, + DateUnit.YEAR, + Instant((2023, 2, 28)), + ], + [ + Instant((2020, 2, 29)), + 3, + DateUnit.MONTH, + Instant((2020, 5, 29)), + ], + [ + Instant((2020, 2, 29)), + 3, + DateUnit.WEEK, + Instant((2020, 3, 21)), + ], [Instant((2020, 2, 29)), 3, DateUnit.DAY, Instant((2020, 3, 3))], - [Instant((2020, 2, 29)), 3, DateUnit.WEEKDAY, Instant((2020, 3, 3))], + [ + Instant((2020, 2, 29)), + 3, + DateUnit.WEEKDAY, + Instant((2020, 3, 3)), + ], ], ) def test_offset(instant, offset, unit, expected): diff --git a/openfisca_core/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index 89fd47b441..65dd6afde1 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -14,7 +14,6 @@ from openfisca_core import periods, simulations from openfisca_core.entities import build_entity -from openfisca_core.periods import DateUnit from openfisca_core.taxbenefitsystems import TaxBenefitSystem from openfisca_core.tools import assert_near from openfisca_core.variables import Variable @@ -81,7 +80,7 @@ class city_code(Variable): value_type = "FixedStr" max_length = 5 entity = Famille - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY label = """Code INSEE "city_code" de la commune de résidence de la famille""" @@ -115,7 +114,7 @@ class dom_tom(Variable): label = "La famille habite-t-elle les DOM-TOM ?" def formula(self, simulation, period): - period = period.start.period(DateUnit.YEAR).offset("first-of") + period = period.start.period(periods.YEAR).offset("first-of") city_code = simulation.calculate("city_code", period) return np.logical_or(startswith(city_code, "97"), startswith(city_code, "98")) @@ -126,7 +125,7 @@ class revenu_disponible(Variable): label = "Revenu disponible de l'individu" def formula(self, simulation, period): - period = period.start.period(DateUnit.YEAR).offset("first-of") + period = period.start.period(periods.YEAR).offset("first-of") rsa = simulation.calculate("rsa", period) salaire_imposable = simulation.calculate("salaire_imposable", period) return rsa + salaire_imposable * 0.7 @@ -138,17 +137,17 @@ class rsa(Variable): label = "RSA" def formula_2010_01_01(self, simulation, period): - period = period.start.period(DateUnit.MONTH).offset("first-of") + period = period.start.period(periods.MONTH).offset("first-of") salaire_imposable = simulation.calculate("salaire_imposable", period) return (salaire_imposable < 500) * 100.0 def formula_2011_01_01(self, simulation, period): - period = period.start.period(DateUnit.MONTH).offset("first-of") + period = period.start.period(periods.MONTH).offset("first-of") salaire_imposable = simulation.calculate("salaire_imposable", period) return (salaire_imposable < 500) * 200.0 def formula_2013_01_01(self, simulation, period): - period = period.start.period(DateUnit.MONTH).offset("first-of") + period = period.start.period(periods.MONTH).offset("first-of") salaire_imposable = simulation.calculate("salaire_imposable", period) return (salaire_imposable < 500) * 300 @@ -159,7 +158,7 @@ class salaire_imposable(Variable): label = "Salaire imposable" def formula(individu, period): - period = period.start.period(DateUnit.YEAR).offset("first-of") + period = period.start.period(periods.YEAR).offset("first-of") dom_tom = individu.famille("dom_tom", period) salaire_net = individu("salaire_net", period) return salaire_net * 0.9 - 100 * dom_tom @@ -171,7 +170,7 @@ class salaire_net(Variable): label = "Salaire net" def formula(self, simulation, period): - period = period.start.period(DateUnit.YEAR).offset("first-of") + period = period.start.period(periods.YEAR).offset("first-of") salaire_brut = simulation.calculate("salaire_brut", period) return salaire_brut * 0.8 diff --git a/openfisca_core/simulations/__init__.py b/openfisca_core/simulations/__init__.py index 670b922ebb..a03d846ddf 100644 --- a/openfisca_core/simulations/__init__.py +++ b/openfisca_core/simulations/__init__.py @@ -21,20 +21,17 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from openfisca_core.errors import ( # noqa: F401 - CycleError, - NaNCreationError, - SpiralError, -) +from openfisca_core.errors import CycleError, NaNCreationError, SpiralError -from .helpers import ( # noqa: F401 +from . import types +from .helpers import ( calculate_output_add, calculate_output_divide, check_type, transform_to_strict_syntax, ) -from .simulation import Simulation # noqa: F401 -from .simulation_builder import SimulationBuilder # noqa: F401 +from .simulation import Simulation +from .simulation_builder import SimulationBuilder __all__ = [ "CycleError", @@ -46,4 +43,5 @@ "calculate_output_divide", "check_type", "transform_to_strict_syntax", + "types", ] diff --git a/openfisca_core/simulations/_build_default_simulation.py b/openfisca_core/simulations/_build_default_simulation.py index f99c1d210a..f677bdd2e4 100644 --- a/openfisca_core/simulations/_build_default_simulation.py +++ b/openfisca_core/simulations/_build_default_simulation.py @@ -1,17 +1,24 @@ """This module contains the _BuildDefaultSimulation class.""" from typing import Union -from typing_extensions import Self +from typing_extensions import Self, TypeAlias import numpy from .simulation import Simulation -from .typing import Entity, Population, TaxBenefitSystem +from .types import Entity, Population, TaxBenefitSystem + +Populations: TypeAlias = dict[str, Population[Entity]] class _BuildDefaultSimulation: """Build a default simulation. + Attributes: + count(int): The number of periods. + populations(Populations): The built populations. + simulation(Simulation): The built simulation. + Args: tax_benefit_system(TaxBenefitSystem): The tax-benefit system. count(int): The number of periods. @@ -61,7 +68,7 @@ def add_count(self) -> Self: """Add the number of Population to the simulation. Returns: - _BuildDefaultSimulation: The builder. + Self: The builder. Examples: >>> from openfisca_core import entities, taxbenefitsystems @@ -94,7 +101,7 @@ def add_ids(self) -> Self: """Add the populations ids to the simulation. Returns: - _BuildDefaultSimulation: The builder. + Self: The builder. Examples: >>> from openfisca_core import entities, taxbenefitsystems @@ -129,7 +136,7 @@ def add_members_entity_id(self) -> Self: Each SingleEntity has its own GroupEntity. Returns: - _BuildDefaultSimulation: The builder. + Self: The builder. Examples: >>> from openfisca_core import entities, taxbenefitsystems diff --git a/openfisca_core/simulations/_build_from_variables.py b/openfisca_core/simulations/_build_from_variables.py index 60ff6148e7..8dad0f11e0 100644 --- a/openfisca_core/simulations/_build_from_variables.py +++ b/openfisca_core/simulations/_build_from_variables.py @@ -2,14 +2,15 @@ from __future__ import annotations +from collections.abc import Sized from typing_extensions import Self from openfisca_core import errors from ._build_default_simulation import _BuildDefaultSimulation -from ._type_guards import is_variable_dated +from ._guards import is_variable_dated from .simulation import Simulation -from .typing import Entity, Population, TaxBenefitSystem, Variables +from .types import Entity, Population, TaxBenefitSystem, Variables class _BuildFromVariables: @@ -99,7 +100,7 @@ def add_dated_values(self) -> Self: """Add the dated input values to the Simulation. Returns: - _BuildFromVariables: The builder. + Self: The builder. Examples: >>> from openfisca_core import entities, periods, taxbenefitsystems, variables @@ -151,7 +152,7 @@ def add_undated_values(self) -> Self: """Add the undated input values to the Simulation. Returns: - _BuildFromVariables: The builder. + Self: The builder. Raises: SituationParsingError: If there is not a default period set. @@ -184,7 +185,7 @@ def add_undated_values(self) -> Self: >>> builder = _BuildFromVariables(tax_benefit_system, variables) >>> builder.add_undated_values() Traceback (most recent call last): - openfisca_core.errors.situation_parsing_error.SituationParsingError + openfisca_core.errors.situation_parsing_error.SituationParsingEr... >>> builder.default_period = period >>> builder.add_undated_values() <..._BuildFromVariables object at ...> @@ -218,7 +219,7 @@ def add_undated_values(self) -> Self: def _person_count(params: Variables) -> int: try: - first_value = next(iter(params.values())) + first_value: object = next(iter(params.values())) if isinstance(first_value, dict): first_value = next(iter(first_value.values())) @@ -226,7 +227,10 @@ def _person_count(params: Variables) -> int: if isinstance(first_value, str): return 1 - return len(first_value) + if isinstance(first_value, Sized): + return len(first_value) + + raise NotImplementedError except Exception: return 1 diff --git a/openfisca_core/simulations/_type_guards.py b/openfisca_core/simulations/_guards.py similarity index 99% rename from openfisca_core/simulations/_type_guards.py rename to openfisca_core/simulations/_guards.py index c34361041a..79e0c85840 100644 --- a/openfisca_core/simulations/_type_guards.py +++ b/openfisca_core/simulations/_guards.py @@ -5,7 +5,7 @@ from typing import Iterable from typing_extensions import TypeGuard -from .typing import ( +from .types import ( Axes, DatedVariable, FullySpecifiedEntities, diff --git a/openfisca_core/simulations/_rules.py b/openfisca_core/simulations/_rules.py new file mode 100644 index 0000000000..d602c5c7a3 --- /dev/null +++ b/openfisca_core/simulations/_rules.py @@ -0,0 +1,119 @@ +"""Rules for simulations, aka business invariants.""" + +from typing import TypedDict, Union +from typing_extensions import TypeAlias + +from openfisca_core import commons, periods + +from .types import Failure, Period, Success, Variable + +#: Type alias for an either monad. +Either: TypeAlias = Union[Failure[str], Success["_State"]] + + +class _State(TypedDict): + """State of the rule-checking.""" + + #: The variable to check. + variable: Variable + + #: The period to check. + period: Period + + +def _check_periods_compatibility_1(state: _State) -> Either: + """When definition period is month/day and period is week. + + Examples: + >>> from openfisca_core import entities, periods, variables + + >>> entity = entities.SingleEntity("", "", "", "") + + >>> class Variable(variables.Variable): + ... definition_period = periods.WEEK + ... entity = entity + ... value_type = int + + >>> variable = Variable() + >>> period = periods.period("2020-W01") + >>> state = {"variable": variable, "period": period} + + >>> _check_periods_compatibility_1(state) + Success(_value={'variable': ..., 'period': ...}) + + >>> variable.definition_period = periods.MONTH + >>> _check_periods_compatibility_1(state) + Failure(_value="Unable to compute variable 'Variable' for period 2...") + + Args: + state(_State): The state of the rule-checking. + + Returns: + Either: The result of the rule-checking. + + """ + + variable = state["variable"] + period = state["period"] + + if ( + variable.definition_period in (periods.MONTH, periods.DAY) + and period.unit == periods.WEEK + ): + return commons.either.fail( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year'." + ) + + return commons.either.succeed(state) + + +def _check_periods_compatibility_2(state: _State) -> Either: + """When definition period is week/weekday and period is month. + + Examples: + >>> from openfisca_core import entities, periods, variables + + >>> entity = entities.SingleEntity("", "", "", "") + + >>> class Variable(variables.Variable): + ... definition_period = periods.YEAR + ... entity = entity + ... value_type = int + + >>> variable = Variable() + >>> period = periods.period("2020-01") + >>> state = {"variable": variable, "period": period} + + >>> _check_periods_compatibility_2(state) + Success(_value={'variable': ..., 'period': ...}) + + >>> variable.definition_period = periods.WEEKDAY + >>> _check_periods_compatibility_2(state) + Failure(_value="Unable to compute variable 'Variable' for period 2...") + + Args: + state(_State): The state of the rule-checking. + + Returns: + Either: The result of the rule-checking. + + """ + + variable = state["variable"] + period = state["period"] + + if ( + variable.definition_period in (periods.WEEK, periods.WEEKDAY) + and period.unit == periods.MONTH + ): + return commons.either.fail( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year' or 'period.first_week'." + ) + + return commons.either.succeed(state) diff --git a/openfisca_core/simulations/helpers.py b/openfisca_core/simulations/helpers.py index d5984d88b6..edc3fa1ad3 100644 --- a/openfisca_core/simulations/helpers.py +++ b/openfisca_core/simulations/helpers.py @@ -2,7 +2,7 @@ from openfisca_core import errors -from .typing import ParamsWithoutAxes +from .types import ParamsWithoutAxes def calculate_output_add(simulation, variable_name: str, period): @@ -58,7 +58,7 @@ def check_unexpected_entities( >>> check_unexpected_entities(params, entities) Traceback (most recent call last): - openfisca_core.errors.situation_parsing_error.SituationParsingError + openfisca_core.errors.situation_parsing_error.SituationParsingError... """ diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 93becda960..4e25ba8d31 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -175,6 +175,29 @@ def calculate_add(self, variable_name: str, period): if period is not None and not isinstance(period, periods.Period): period = periods.period(period) + # Rule out incompatible periods. + if ( + variable.definition_period in (periods.MONTH, periods.DAY) + and period.unit == periods.WEEK + ): + raise ValueError( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year'." + ) + + if ( + variable.definition_period in (periods.WEEK, periods.WEEKDAY) + and period.unit == periods.MONTH + ): + raise ValueError( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year' or 'period.first_week'." + ) + # Check that the requested period matches definition_period if periods.unit_weight(variable.definition_period) > periods.unit_weight( period.unit @@ -186,9 +209,7 @@ def calculate_add(self, variable_name: str, period): f"DIVIDE option to get an estimate of {variable.name}." ) - if variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar - ): + if variable.definition_period not in (periods.ISOFORMAT + periods.ISOCALENDAR): raise ValueError( f"Unable to ADD constant variable '{variable.name}' over " f"the period {period}: eternal variables can't be summed " @@ -225,9 +246,7 @@ def calculate_divide(self, variable_name: str, period): f"ADD option to get an estimate of {variable.name}." ) - if variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar - ): + if variable.definition_period not in (periods.ISOFORMAT + periods.ISOCALENDAR): raise ValueError( f"Unable to DIVIDE constant variable '{variable.name}' over " f"the period {period}: eternal variables can't be divided " @@ -235,8 +254,7 @@ def calculate_divide(self, variable_name: str, period): ) if ( - period.unit - not in (periods.DateUnit.isoformat + periods.DateUnit.isocalendar) + period.unit not in (periods.ISOFORMAT + periods.ISOCALENDAR) or period.size != 1 ): raise ValueError( @@ -463,23 +481,23 @@ def delete_arrays(self, variable, period=None): Example: - >>> from openfisca_country_template import CountryTaxBenefitSystem - >>> simulation = Simulation(CountryTaxBenefitSystem()) - >>> simulation.set_input('age', '2018-04', [12, 14]) - >>> simulation.set_input('age', '2018-05', [13, 14]) - >>> simulation.get_array('age', '2018-05') - array([13, 14], dtype=int32) - >>> simulation.delete_arrays('age', '2018-05') - >>> simulation.get_array('age', '2018-04') - array([12, 14], dtype=int32) - >>> simulation.get_array('age', '2018-05') is None - True - >>> simulation.set_input('age', '2018-05', [13, 14]) - >>> simulation.delete_arrays('age') - >>> simulation.get_array('age', '2018-04') is None - True - >>> simulation.get_array('age', '2018-05') is None - True + # >>> from openfisca_country_template import CountryTaxBenefitSystem + # >>> simulation = Simulation(CountryTaxBenefitSystem()) + # >>> simulation.set_input('age', '2018-04', [12, 14]) + # >>> simulation.set_input('age', '2018-05', [13, 14]) + # >>> simulation.get_array('age', '2018-05') + # array([13, 14], dtype=int32) + # >>> simulation.delete_arrays('age', '2018-05') + # >>> simulation.get_array('age', '2018-04') + # array([12, 14], dtype=int32) + # >>> simulation.get_array('age', '2018-05') is None + # True + # >>> simulation.set_input('age', '2018-05', [13, 14]) + # >>> simulation.delete_arrays('age') + # >>> simulation.get_array('age', '2018-04') is None + # True + # >>> simulation.get_array('age', '2018-05') is None + # True """ self.get_holder(variable).delete_arrays(period) @@ -491,12 +509,12 @@ def get_known_periods(self, variable): Example: - >>> from openfisca_country_template import CountryTaxBenefitSystem - >>> simulation = Simulation(CountryTaxBenefitSystem()) - >>> simulation.set_input('age', '2018-04', [12, 14]) - >>> simulation.set_input('age', '2018-05', [13, 14]) - >>> simulation.get_known_periods('age') - [Period((u'month', Instant((2018, 5, 1)), 1)), Period((u'month', Instant((2018, 4, 1)), 1))] + # >>> from openfisca_country_template import CountryTaxBenefitSystem + # >>> simulation = Simulation(CountryTaxBenefitSystem()) + # >>> simulation.set_input('age', '2018-04', [12, 14]) + # >>> simulation.set_input('age', '2018-05', [13, 14]) + # >>> simulation.get_known_periods('age') + # [Period((u'month', Instant((2018, 5, 1)), 1)), Period((u'month', Instant((2018, 4, 1)), 1))] """ return self.get_holder(variable).get_known_periods() @@ -509,11 +527,11 @@ def set_input(self, variable_name: str, period, value): :param period: the period for which the value is setted Example: - >>> from openfisca_country_template import CountryTaxBenefitSystem - >>> simulation = Simulation(CountryTaxBenefitSystem()) - >>> simulation.set_input('age', '2018-04', [12, 14]) - >>> simulation.get_array('age', '2018-04') - array([12, 14], dtype=int32) + # >>> from openfisca_country_template import CountryTaxBenefitSystem + # >>> simulation = Simulation(CountryTaxBenefitSystem()) + # >>> simulation.set_input('age', '2018-04', [12, 14]) + # >>> simulation.get_array('age', '2018-04') + # array([12, 14], dtype=int32) 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 `_. """ diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index c42d0e4f22..fcf447704d 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -14,14 +14,14 @@ from . import helpers from ._build_default_simulation import _BuildDefaultSimulation from ._build_from_variables import _BuildFromVariables -from ._type_guards import ( +from ._guards import ( are_entities_fully_specified, are_entities_short_form, are_entities_specified, has_axes, ) from .simulation import Simulation -from .typing import ( +from .types import ( Axis, Entity, FullySpecifiedEntities, diff --git a/openfisca_core/simulations/tests/__init__.py b/openfisca_core/simulations/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/simulations/tests/test_invariants.py b/openfisca_core/simulations/tests/test_invariants.py new file mode 100644 index 0000000000..ff9564f6e2 --- /dev/null +++ b/openfisca_core/simulations/tests/test_invariants.py @@ -0,0 +1,248 @@ +from typing import TypedDict + +from openfisca_core.simulations.types import Variable + +import enum + +import pytest + +from openfisca_core import periods + + +class EitherTag(enum.Enum): + FAILURE = enum.auto() + SUCCESS = enum.auto() + + +class Either: + def __init__(self, value, tag=EitherTag.SUCCESS): + self._value = value + self._tag = tag + + @property + def isSuccess(self): + return self._tag == EitherTag.SUCCESS + + @property + def isFailure(self): + return self._tag == EitherTag.FAILURE + + def map(self, f): + if self.isSuccess: + return Either.succeed(f(**self.unwrap())) + + return self + + def join(self): + if self.isSuccess and isinstance(self.unwrap(), Either): + return self.unwrap() + + return self + + def then(self, f): + return self.map(f).join() + + def unwrap(self): + return self._value + + @staticmethod + def fail(value): + return Either(value, EitherTag.FAILURE) + + @staticmethod + def succeed(value): + return Either(value, EitherTag.SUCCESS) + + +class TestVariable(Variable): + def __init__(self, name, definition_period): + self.name = name + self.definition_period = definition_period + + +class ValidationParams(TypedDict): + variable: Variable + period: periods.Period + + +def are_periods_compatible_1(**params): + variable = params["variable"] + period = params["period"] + + if ( + variable.definition_period in (periods.MONTH, periods.DAY) + and period.unit == periods.WEEK + ): + return Either.fail( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year'." + ) + + return Either.succeed(params) + + +def are_periods_compatible_2(**params): + variable = params["variable"] + period = params["period"] + + if ( + variable.definition_period in (periods.WEEK, periods.WEEKDAY) + and period.unit == periods.MONTH + ): + return Either.fail( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year' or 'period.first_week'." + ) + + return Either.succeed(params) + + +def derive_calculate_add(**params): + either = ( + Either(params).then(are_periods_compatible_1).then(are_periods_compatible_2) + ) + + if either.isSuccess: + return either.unwrap() + + raise ValueError(either.unwrap()) + + +@pytest.mark.parametrize( + "period_unit, period_str, expected", + [ + (periods.YEAR, "2020", True), + (periods.YEAR, "2020-01", True), + (periods.YEAR, "2020-01-01", True), + (periods.YEAR, "2020-W01", True), + (periods.YEAR, "2020-W01-1", True), + (periods.MONTH, "2020", True), + (periods.MONTH, "2020-01", True), + (periods.MONTH, "2020-01-01", True), + (periods.MONTH, "2020-W01", False), + (periods.MONTH, "2020-W01-1", True), + (periods.DAY, "2020", True), + (periods.DAY, "2020-01", True), + (periods.DAY, "2020-01-01", True), + (periods.DAY, "2020-W01", False), + (periods.DAY, "2020-W01-1", True), + (periods.WEEK, "2020", True), + (periods.WEEK, "2020-01", True), + (periods.WEEK, "2020-01-01", True), + (periods.WEEK, "2020-W01", True), + (periods.WEEK, "2020-W01-1", True), + (periods.WEEKDAY, "2020", True), + (periods.WEEKDAY, "2020-01", True), + (periods.WEEKDAY, "2020-01-01", True), + (periods.WEEKDAY, "2020-W01", True), + (periods.WEEKDAY, "2020-W01-1", True), + ], +) +def test_are_periods_compatible_1(period_unit, period_str, expected): + variable = TestVariable("variable", period_unit) + period = periods.period(period_str) + either = are_periods_compatible_1(variable=variable, period=period) + assert either.isSuccess is expected + + +@pytest.mark.parametrize( + "period_unit, period_str, expected", + [ + (periods.YEAR, "2020", True), + (periods.YEAR, "2020-01", True), + (periods.YEAR, "2020-01-01", True), + (periods.YEAR, "2020-W01", True), + (periods.YEAR, "2020-W01-1", True), + (periods.MONTH, "2020", True), + (periods.MONTH, "2020-01", True), + (periods.MONTH, "2020-01-01", True), + (periods.MONTH, "2020-W01", True), + (periods.MONTH, "2020-W01-1", True), + (periods.DAY, "2020", True), + (periods.DAY, "2020-01", True), + (periods.DAY, "2020-01-01", True), + (periods.DAY, "2020-W01", True), + (periods.DAY, "2020-W01-1", True), + (periods.WEEK, "2020", True), + (periods.WEEK, "2020-01", False), + (periods.WEEK, "2020-01-01", True), + (periods.WEEK, "2020-W01", True), + (periods.WEEK, "2020-W01-1", True), + (periods.WEEKDAY, "2020", True), + (periods.WEEKDAY, "2020-01", False), + (periods.WEEKDAY, "2020-01-01", True), + (periods.WEEKDAY, "2020-W01", True), + (periods.WEEKDAY, "2020-W01-1", True), + ], +) +def test_are_periods_compatible_2(period_unit, period_str, expected): + variable = TestVariable("variable", period_unit) + period = periods.period(period_str) + either = are_periods_compatible_2(variable=variable, period=period) + assert either.isSuccess is expected + + +@pytest.mark.parametrize( + "period_unit, period_str", + [ + (periods.YEAR, "2020"), + (periods.YEAR, "2020-01"), + (periods.YEAR, "2020-01-01"), + (periods.YEAR, "2020-W01"), + (periods.YEAR, "2020-W01-1"), + (periods.MONTH, "2020"), + (periods.MONTH, "2020-01"), + (periods.MONTH, "2020-01-01"), + (periods.MONTH, "2020-W01-1"), + (periods.DAY, "2020"), + (periods.DAY, "2020-01"), + (periods.DAY, "2020-01-01"), + (periods.DAY, "2020-W01-1"), + (periods.WEEK, "2020"), + (periods.WEEK, "2020-01-01"), + (periods.WEEK, "2020-W01"), + (periods.WEEK, "2020-W01-1"), + (periods.WEEKDAY, "2020"), + (periods.WEEKDAY, "2020-01-01"), + (periods.WEEKDAY, "2020-W01"), + (periods.WEEKDAY, "2020-W01-1"), + ], +) +def test_derive_calculate_add(period_unit, period_str): + variable = TestVariable("variable", period_unit) + period = periods.period(period_str) + assert derive_calculate_add(variable=variable, period=period) + + +@pytest.mark.parametrize( + "period_unit, period_str", + [ + (periods.MONTH, "2020-W01"), + (periods.DAY, "2020-W01"), + ], +) +def test_derive_calculate_add_with_invalid_period_1(period_unit, period_str): + variable = TestVariable("variable", period_unit) + period = periods.period(period_str) + with pytest.raises(ValueError) as error: + derive_calculate_add(variable=variable, period=period) + assert "period.first_week" not in str(error.value) + + +@pytest.mark.parametrize( + "period_unit, period_str", + [ + (periods.WEEK, "2020-01"), + (periods.WEEKDAY, "2020-01"), + ], +) +def test_derive_calculate_add_with_invalid_period_2(period_unit, period_str): + variable = TestVariable("variable", period_unit) + period = periods.period(period_str) + with pytest.raises(ValueError) as error: + derive_calculate_add(variable=variable, period=period) + assert "period.first_week" in str(error.value) diff --git a/openfisca_core/simulations/tests/test_rules.py b/openfisca_core/simulations/tests/test_rules.py new file mode 100644 index 0000000000..e2c0946508 --- /dev/null +++ b/openfisca_core/simulations/tests/test_rules.py @@ -0,0 +1,163 @@ +from openfisca_core.simulations.types import Variable + +import pytest + +from openfisca_core import commons, periods +from openfisca_core.simulations import _rules + + +class TestVariable(Variable): + def __init__(self, name, definition_period): + self.name = name + self.definition_period = definition_period + + +def derive_calculate_add(state): + either = ( + commons.either(state) + .then(_rules._check_periods_compatibility_1) + .then(_rules._check_periods_compatibility_2) + ) + + if either.is_success: + return either.unwrap() + + raise ValueError(either.unwrap()) + + +@pytest.mark.parametrize( + "period_unit, period_str, expected", + [ + (periods.YEAR, "2020", True), + (periods.YEAR, "2020-01", True), + (periods.YEAR, "2020-01-01", True), + (periods.YEAR, "2020-W01", True), + (periods.YEAR, "2020-W01-1", True), + (periods.MONTH, "2020", True), + (periods.MONTH, "2020-01", True), + (periods.MONTH, "2020-01-01", True), + (periods.MONTH, "2020-W01", False), + (periods.MONTH, "2020-W01-1", True), + (periods.DAY, "2020", True), + (periods.DAY, "2020-01", True), + (periods.DAY, "2020-01-01", True), + (periods.DAY, "2020-W01", False), + (periods.DAY, "2020-W01-1", True), + (periods.WEEK, "2020", True), + (periods.WEEK, "2020-01", True), + (periods.WEEK, "2020-01-01", True), + (periods.WEEK, "2020-W01", True), + (periods.WEEK, "2020-W01-1", True), + (periods.WEEKDAY, "2020", True), + (periods.WEEKDAY, "2020-01", True), + (periods.WEEKDAY, "2020-01-01", True), + (periods.WEEKDAY, "2020-W01", True), + (periods.WEEKDAY, "2020-W01-1", True), + ], +) +def test_are_periods_compatible_1(period_unit, period_str, expected): + variable = TestVariable("variable", period_unit) + period = periods.period(period_str) + either = _rules._check_periods_compatibility_1( + {"variable": variable, "period": period} + ) + assert either.is_success is expected + + +# @pytest.mark.parametrize( +# "period_unit, period_str, expected", +# [ +# (periods.YEAR, "2020", True), +# (periods.YEAR, "2020-01", True), +# (periods.YEAR, "2020-01-01", True), +# (periods.YEAR, "2020-W01", True), +# (periods.YEAR, "2020-W01-1", True), +# (periods.MONTH, "2020", True), +# (periods.MONTH, "2020-01", True), +# (periods.MONTH, "2020-01-01", True), +# (periods.MONTH, "2020-W01", True), +# (periods.MONTH, "2020-W01-1", True), +# (periods.DAY, "2020", True), +# (periods.DAY, "2020-01", True), +# (periods.DAY, "2020-01-01", True), +# (periods.DAY, "2020-W01", True), +# (periods.DAY, "2020-W01-1", True), +# (periods.WEEK, "2020", True), +# (periods.WEEK, "2020-01", False), +# (periods.WEEK, "2020-01-01", True), +# (periods.WEEK, "2020-W01", True), +# (periods.WEEK, "2020-W01-1", True), +# (periods.WEEKDAY, "2020", True), +# (periods.WEEKDAY, "2020-01", False), +# (periods.WEEKDAY, "2020-01-01", True), +# (periods.WEEKDAY, "2020-W01", True), +# (periods.WEEKDAY, "2020-W01-1", True), +# ], +# ) +# def test_are_periods_compatible_2(period_unit, period_str, expected): +# variable = TestVariable("variable", period_unit) +# period = periods.period(period_str) +# either = _rules._check_periods_compatibility_2({variable: variable, period: period}) +# assert either.is_success is expected +# +# +# @pytest.mark.parametrize( +# "period_unit, period_str", +# [ +# (periods.YEAR, "2020"), +# (periods.YEAR, "2020-01"), +# (periods.YEAR, "2020-01-01"), +# (periods.YEAR, "2020-W01"), +# (periods.YEAR, "2020-W01-1"), +# (periods.MONTH, "2020"), +# (periods.MONTH, "2020-01"), +# (periods.MONTH, "2020-01-01"), +# (periods.MONTH, "2020-W01-1"), +# (periods.DAY, "2020"), +# (periods.DAY, "2020-01"), +# (periods.DAY, "2020-01-01"), +# (periods.DAY, "2020-W01-1"), +# (periods.WEEK, "2020"), +# (periods.WEEK, "2020-01-01"), +# (periods.WEEK, "2020-W01"), +# (periods.WEEK, "2020-W01-1"), +# (periods.WEEKDAY, "2020"), +# (periods.WEEKDAY, "2020-01-01"), +# (periods.WEEKDAY, "2020-W01"), +# (periods.WEEKDAY, "2020-W01-1"), +# ], +# ) +# def test_derive_calculate_add(period_unit, period_str): +# variable = TestVariable("variable", period_unit) +# period = periods.period(period_str) +# assert derive_calculate_add({variable: variable, period: period}) +# +# +# @pytest.mark.parametrize( +# "period_unit, period_str", +# [ +# (periods.MONTH, "2020-W01"), +# (periods.DAY, "2020-W01"), +# ], +# ) +# def test_derive_calculate_add_with_invalid_period_1(period_unit, period_str): +# variable = TestVariable("variable", period_unit) +# period = periods.period(period_str) +# with pytest.raises(ValueError) as error: +# derive_calculate_add({variable: variable, period: period}) +# assert "period.first_week" not in str(error.value) +# +# +# @pytest.mark.parametrize( +# "period_unit, period_str", +# [ +# (periods.WEEK, "2020-01"), +# (periods.WEEKDAY, "2020-01"), +# ], +# ) +# def test_derive_calculate_add_with_invalid_period_2(period_unit, period_str): +# variable = TestVariable("variable", period_unit) +# period = periods.period(period_str) +# with pytest.raises(ValueError) as error: +# derive_calculate_add({variable: variable, period: period}) +# assert "period.first_week" in str(error.value) diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/types.py similarity index 65% rename from openfisca_core/simulations/typing.py rename to openfisca_core/simulations/types.py index 8603d0d811..81feffabb6 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/types.py @@ -3,10 +3,15 @@ from __future__ import annotations from collections.abc import Iterable, Sequence -from numpy.typing import NDArray as Array from typing import Protocol, TypeVar, TypedDict, Union from typing_extensions import NotRequired, Required, TypeAlias +from openfisca_core.types import Array +from openfisca_core.types import Failure as FailureType +from openfisca_core.types import Period as PeriodType +from openfisca_core.types import Success as SuccessType +from openfisca_core.types import Variable as VariableType + import datetime from abc import abstractmethod @@ -17,67 +22,41 @@ from numpy import int32 as Int from numpy import str_ as String -#: Generic type variables. +# Generic type variables. E = TypeVar("E") G = TypeVar("G", covariant=True) -T = TypeVar("T", Bool, Date, Enum, Float, Int, String, covariant=True) U = TypeVar("U", bool, datetime.date, float, str) V = TypeVar("V", covariant=True) +#: Type variable representing an error. +F = TypeVar("F", covariant=True) -#: Type alias for a simulation dictionary defining the roles. -Roles: TypeAlias = dict[str, Union[str, Iterable[str]]] +#: Type variable representing a value. +A = TypeVar("A", covariant=True) -#: Type alias for a simulation dictionary with undated variables. -UndatedVariable: TypeAlias = dict[str, object] - -#: Type alias for a simulation dictionary with dated variables. -DatedVariable: TypeAlias = dict[str, UndatedVariable] - -#: Type alias for a simulation dictionary with abbreviated entities. -Variables: TypeAlias = dict[str, Union[UndatedVariable, DatedVariable]] - -#: Type alias for a simulation with fully specified single entities. -SingleEntities: TypeAlias = dict[str, dict[str, Variables]] +#: Type alias for numpy arrays values. +Item: TypeAlias = Union[Bool, Date, Enum, Float, Int, String] -#: Type alias for a simulation dictionary with implicit group entities. -ImplicitGroupEntities: TypeAlias = dict[str, Union[Roles, Variables]] -#: Type alias for a simulation dictionary with explicit group entities. -GroupEntities: TypeAlias = dict[str, ImplicitGroupEntities] +# Commons -#: Type alias for a simulation dictionary with fully specified entities. -FullySpecifiedEntities: TypeAlias = Union[SingleEntities, GroupEntities] -#: Type alias for a simulation dictionary with axes parameters. -Axes: TypeAlias = dict[str, Iterable[Iterable["Axis"]]] +class Failure(FailureType[F], Protocol[F]): + ... -#: Type alias for a simulation dictionary without axes parameters. -ParamsWithoutAxes: TypeAlias = Union[ - Variables, ImplicitGroupEntities, FullySpecifiedEntities -] -#: Type alias for a simulation dictionary with axes parameters. -ParamsWithAxes: TypeAlias = Union[Axes, ParamsWithoutAxes] +class Success(SuccessType[A], Protocol[A]): + ... -#: Type alias for a simulation dictionary with all the possible scenarios. -Params: TypeAlias = ParamsWithAxes +# Entities -class Axis(TypedDict, total=False): - """Interface representing an axis of a simulation.""" - count: Required[int] - index: NotRequired[int] - max: Required[float] - min: Required[float] - name: Required[str] - period: NotRequired[str | int] +#: Type alias for a simulation dictionary defining the roles. +Roles: TypeAlias = dict[str, Union[str, Iterable[str]]] class Entity(Protocol): - """Interface representing an entity of a simulation.""" - key: str plural: str | None @@ -85,115 +64,168 @@ def get_variable( self, __variable_name: str, __check_existence: bool = ..., - ) -> Variable[T] | None: - """Get a variable.""" + ) -> Variable | None: + ... class SingleEntity(Entity, Protocol): - """Interface representing a single entity of a simulation.""" + ... class GroupEntity(Entity, Protocol): - """Interface representing a group entity of a simulation.""" - @property @abstractmethod def flattened_roles(self) -> Iterable[Role[G]]: - """Get the flattened roles of the GroupEntity.""" + ... -class Holder(Protocol[V]): - """Interface representing a holder of a simulation's computed values.""" +class Role(Protocol[G]): + ... + + +# Holders + +class Holder(Protocol[V]): @property @abstractmethod - def variable(self) -> Variable[T]: - """Get the Variable of the Holder.""" + def variable(self) -> Variable: + ... - def get_array(self, __period: str) -> Array[T] | None: - """Get the values of the Variable for a given Period.""" + def get_array(self, __period: str) -> Array[Item] | None: + ... def set_input( self, __period: Period, - __array: Array[T] | Sequence[U], - ) -> Array[T] | None: - """Set values for a Variable for a given Period.""" + __array: Array[Item] | Sequence[U], + ) -> Array[Item] | None: + ... -class Period(Protocol): - """Interface representing a period of a simulation.""" +# Periods -class Population(Protocol[E]): - """Interface representing a data vector of an Entity.""" +class Period(PeriodType, Protocol): + ... + +# Populations + + +class Population(Protocol[E]): count: int entity: E ids: Array[String] def get_holder(self, __variable_name: str) -> Holder[V]: - """Get the holder of a Variable.""" + ... class SinglePopulation(Population[E], Protocol): - """Interface representing a data vector of a SingleEntity.""" + ... class GroupPopulation(Population[E], Protocol): - """Interface representing a data vector of a GroupEntity.""" - members_entity_id: Array[String] def nb_persons(self, __role: Role[G] | None = ...) -> int: - """Get the number of persons for a given Role.""" + ... -class Role(Protocol[G]): - """Interface representing a role of the group entities of a simulation.""" +# Simulations -class TaxBenefitSystem(Protocol): - """Interface representing a tax-benefit system.""" +#: Type alias for a simulation dictionary with undated variables. +UndatedVariable: TypeAlias = dict[str, object] + +#: Type alias for a simulation dictionary with dated variables. +DatedVariable: TypeAlias = dict[str, UndatedVariable] + +#: Type alias for a simulation dictionary with abbreviated entities. +Variables: TypeAlias = dict[str, Union[UndatedVariable, DatedVariable]] + +#: Type alias for a simulation with fully specified single entities. +SingleEntities: TypeAlias = dict[str, dict[str, Variables]] + +#: Type alias for a simulation dictionary with implicit group entities. +ImplicitGroupEntities: TypeAlias = dict[str, Union[Roles, Variables]] + +#: Type alias for a simulation dictionary with explicit group entities. +GroupEntities: TypeAlias = dict[str, ImplicitGroupEntities] + +#: Type alias for a simulation dictionary with fully specified entities. +FullySpecifiedEntities: TypeAlias = Union[SingleEntities, GroupEntities] + +#: Type alias for a simulation dictionary with axes parameters. +Axes: TypeAlias = dict[str, Iterable[Iterable["Axis"]]] +#: Type alias for a simulation dictionary without axes parameters. +ParamsWithoutAxes: TypeAlias = Union[ + Variables, ImplicitGroupEntities, FullySpecifiedEntities +] + +#: Type alias for a simulation dictionary with axes parameters. +ParamsWithAxes: TypeAlias = Union[Axes, ParamsWithoutAxes] + +#: Type alias for a simulation dictionary with all the possible scenarios. +Params: TypeAlias = ParamsWithAxes + + +class Axis(TypedDict, total=False): + count: Required[int] + index: NotRequired[int] + max: Required[float] + min: Required[float] + name: Required[str] + period: NotRequired[str | int] + + +# Tax-Benefit systems + + +class TaxBenefitSystem(Protocol): @property @abstractmethod def person_entity(self) -> SingleEntity: - """Get the person entity of the tax-benefit system.""" + ... @person_entity.setter @abstractmethod def person_entity(self, person_entity: SingleEntity) -> None: - """Set the person entity of the tax-benefit system.""" + ... @property @abstractmethod def variables(self) -> dict[str, V]: - """Get the variables of the tax-benefit system.""" + ... def entities_by_singular(self) -> dict[str, E]: - """Get the singular form of the entities' keys.""" + ... def entities_plural(self) -> Iterable[str]: - """Get the plural form of the entities' keys.""" + ... def get_variable( self, __variable_name: str, __check_existence: bool = ..., ) -> V | None: - """Get a variable.""" + ... def instantiate_entities( self, ) -> dict[str, Population[E]]: - """Instantiate the populations of each Entity.""" + ... + +# Variables -class Variable(Protocol[T]): - """Interface representing a variable of a tax-benefit system.""" +class Variable(VariableType, Protocol): + definition_period: str end: str + name: str - def default_array(self, __array_size: int) -> Array[T]: - """Fill an array with the default value of the Variable.""" + def default_array(self, __array_size: int) -> Array[Item]: + ... diff --git a/openfisca_core/tools/simulation_dumper.py b/openfisca_core/tools/simulation_dumper.py index ab21bd79f0..497c7ca031 100644 --- a/openfisca_core/tools/simulation_dumper.py +++ b/openfisca_core/tools/simulation_dumper.py @@ -5,8 +5,8 @@ import numpy as np +from openfisca_core import periods from openfisca_core.data_storage import OnDiskStorage -from openfisca_core.periods import DateUnit from openfisca_core.simulations import Simulation @@ -122,7 +122,7 @@ def _restore_holder(simulation, variable, directory): storage_dir = os.path.join(directory, variable) is_variable_eternal = ( simulation.tax_benefit_system.get_variable(variable).definition_period - == DateUnit.ETERNITY + == periods.ETERNITY ) disk_storage = OnDiskStorage( storage_dir, is_eternal=is_variable_eternal, preserve_storage_dir=True diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types.py similarity index 66% rename from openfisca_core/types/_domain.py rename to openfisca_core/types.py index d324f1b2cf..e31aa8834b 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types.py @@ -1,134 +1,148 @@ from __future__ import annotations import typing_extensions -from typing import Any, Optional -from typing_extensions import Protocol +from collections.abc import Sequence +from numpy.typing import NDArray +from typing import Any, TypeVar +from typing_extensions import Protocol, TypeAlias import abc import numpy +N = TypeVar("N", bound=numpy.generic, covariant=True) + +#: Type representing an numpy array. +Array: TypeAlias = NDArray[N] + +L = TypeVar("L") + +#: Type representing an array-like object. +ArrayLike: TypeAlias = Sequence[L] + +#: Type variable representing an error. +E = TypeVar("E", covariant=True) + +#: Type variable representing a value. +A = TypeVar("A", covariant=True) + + +# Commons + + +class Failure(Protocol[E]): + ... -class Entity(Protocol): - """Entity protocol.""" +class Success(Protocol[A]): + ... + + +# Rest + + +class Entity(Protocol): key: Any plural: Any @abc.abstractmethod def check_role_validity(self, role: Any) -> None: - """Abstract method.""" + ... @abc.abstractmethod def check_variable_defined_for_entity(self, variable_name: Any) -> None: - """Abstract method.""" + ... @abc.abstractmethod def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Optional[Any]: - """Abstract method.""" + ) -> Any | None: + ... class Formula(Protocol): - """Formula protocol.""" - @abc.abstractmethod def __call__( self, population: Population, instant: Instant, params: Params, - ) -> numpy.ndarray: - """Abstract method.""" + ) -> Array[Any]: + ... class Holder(Protocol): - """Holder protocol.""" - @abc.abstractmethod def clone(self, population: Any) -> Holder: - """Abstract method.""" + ... @abc.abstractmethod def get_memory_usage(self) -> Any: - """Abstract method.""" + ... class Instant(Protocol): - """Instant protocol.""" + ... @typing_extensions.runtime_checkable class ParameterNodeAtInstant(Protocol): - """ParameterNodeAtInstant protocol.""" + ... class Params(Protocol): - """Params protocol.""" - @abc.abstractmethod def __call__(self, instant: Instant) -> ParameterNodeAtInstant: - """Abstract method.""" + ... @typing_extensions.runtime_checkable class Period(Protocol): - """Period protocol.""" - @property @abc.abstractmethod def start(self) -> Any: - """Abstract method.""" + ... @property @abc.abstractmethod def unit(self) -> Any: - """Abstract method.""" + ... class Population(Protocol): - """Population protocol.""" - entity: Any @abc.abstractmethod def get_holder(self, variable_name: Any) -> Any: - """Abstract method.""" + ... class Role(Protocol): - """Role protocol.""" - entity: Any subroles: Any class Simulation(Protocol): - """Simulation protocol.""" - @abc.abstractmethod def calculate(self, variable_name: Any, period: Any) -> Any: - """Abstract method.""" + ... @abc.abstractmethod def calculate_add(self, variable_name: Any, period: Any) -> Any: - """Abstract method.""" + ... @abc.abstractmethod def calculate_divide(self, variable_name: Any, period: Any) -> Any: - """Abstract method.""" + ... @abc.abstractmethod - def get_population(self, plural: Optional[Any]) -> Any: - """Abstract method.""" + def get_population(self, plural: Any | None) -> Any: + ... class TaxBenefitSystem(Protocol): - """TaxBenefitSystem protocol.""" - person_entity: Any @abc.abstractmethod @@ -136,11 +150,9 @@ def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Optional[Any]: + ) -> Any | None: """Abstract method.""" class Variable(Protocol): - """Variable protocol.""" - entity: Any diff --git a/openfisca_core/types/__init__.py b/openfisca_core/types/__init__.py deleted file mode 100644 index eb403c46c9..0000000000 --- a/openfisca_core/types/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Data types and protocols used by OpenFisca Core. - -The type definitions included in this sub-package are intended for -contributors, to help them better understand and document contracts -and expected behaviours. - -Official Public API: - * :attr:`.Array` - * ``ArrayLike`` - * :attr:`.Cache` - * :attr:`.Entity` - * :attr:`.Formula` - * :attr:`.Holder` - * :attr:`.Instant` - * :attr:`.ParameterNodeAtInstant` - * :attr:`.Params` - * :attr:`.Period` - * :attr:`.Population` - * :attr:`.Role`, - * :attr:`.Simulation`, - * :attr:`.TaxBenefitSystem` - * :attr:`.Variable` - -Note: - How imports are being used today:: - - from openfisca_core.types import * # Bad - from openfisca_core.types.data_types.arrays import ArrayLike # Bad - - The previous examples provoke cyclic dependency problems, that prevents us - from modularizing the different components of the library, so as to make - them easier to test and to maintain. - - How could them be used after the next major release:: - - from openfisca_core.types import ArrayLike - - ArrayLike # Good: import types as publicly exposed - - .. seealso:: `PEP8#Imports`_ and `OpenFisca's Styleguide`_. - - .. _PEP8#Imports: - https://www.python.org/dev/peps/pep-0008/#imports - - .. _OpenFisca's Styleguide: - https://github.com/openfisca/openfisca-core/blob/master/STYLEGUIDE.md - -""" - -# Official Public API - -from ._data import Array, ArrayLike # noqa: F401 -from ._domain import ( # noqa: F401 - Entity, - Formula, - Holder, - Instant, - ParameterNodeAtInstant, - Params, - Period, - Population, - Role, - Simulation, - TaxBenefitSystem, - Variable, -) - -__all__ = [ - "Array", - "ArrayLike", - "Entity", - "Formula", - "Holder", - "Instant", - "ParameterNodeAtInstant", - "Params", - "Period", - "Population", - "Role", - "Simulation", - "TaxBenefitSystem", - "Variable", -] diff --git a/openfisca_core/types/_data.py b/openfisca_core/types/_data.py deleted file mode 100644 index 928e1b9174..0000000000 --- a/openfisca_core/types/_data.py +++ /dev/null @@ -1,54 +0,0 @@ -# from typing import Sequence, TypeVar, Union -# from nptyping import types, NDArray as Array -from numpy.typing import NDArray as Array # noqa: F401 -from typing import Sequence, TypeVar - -# import numpy - -# NumpyT = TypeVar("NumpyT", numpy.bytes_, numpy.number, numpy.object_, numpy.str_) -T = TypeVar("T", bool, bytes, float, int, object, str) - -# types._ndarray_meta._Type = Union[type, numpy.dtype, TypeVar] - -# ArrayLike = Union[Array[T], Sequence[T]] -ArrayLike = Sequence[T] -""":obj:`typing.Generic`: Type of any castable to :class:`numpy.ndarray`. - -These include any :obj:`numpy.ndarray` and sequences (like -:obj:`list`, :obj:`tuple`, and so on). - -Examples: - >>> ArrayLike[float] - typing.Union[numpy.ndarray, typing.Sequence[float]] - - >>> ArrayLike[str] - typing.Union[numpy.ndarray, typing.Sequence[str]] - -Note: - It is possible since numpy version 1.21 to specify the type of an - array, thanks to `numpy.typing.NDArray`_:: - - from numpy.typing import NDArray - NDArray[numpy.float64] - - `mypy`_ provides `duck type compatibility`_, so an :obj:`int` is - considered to be valid whenever a :obj:`float` is expected. - -Todo: - * Refactor once numpy version >= 1.21 is used. - -.. versionadded:: 35.5.0 - -.. versionchanged:: 35.6.0 - Moved to :mod:`.types` - -.. _mypy: - https://mypy.readthedocs.io/en/stable/ - -.. _duck type compatibility: - https://mypy.readthedocs.io/en/stable/duck_type_compatibility.html - -.. _numpy.typing.NDArray: - https://numpy.org/doc/stable/reference/typing.html#numpy.typing.NDArray - -""" diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index e70c0d05d9..eeddc97ba3 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -14,7 +14,6 @@ from openfisca_core import periods, tools from openfisca_core.entities import Entity, GroupEntity from openfisca_core.indexed_enums import Enum, EnumArray -from openfisca_core.periods import DateUnit, Period from . import config, helpers @@ -136,7 +135,7 @@ def __init__(self, baseline_variable=None): ) self.entity = self.set(attr, "entity", required=True, setter=self.set_entity) self.definition_period = self.set( - attr, "definition_period", required=True, allowed_values=DateUnit + attr, "definition_period", required=True, allowed_values=periods.DateUnit ) self.label = self.set(attr, "label", allowed_type=str, setter=self.set_label) self.end = self.set(attr, "end", allowed_type=str, setter=self.set_end) @@ -374,7 +373,7 @@ def get_introspection_data(cls): def get_formula( self, - period: Union[Instant, Period, str, int] = None, + period: Union[Instant, periods.Period, str, int] = None, ) -> Optional[Formula]: """Returns the formula to compute the variable at the given period. @@ -399,7 +398,7 @@ def get_formula( 1 ] # peekitem gets the 1st key-value tuple (the oldest start_date and formula). Return the formula. - if isinstance(period, Period): + if isinstance(period, periods.Period): instant = period.start else: try: diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 324104f7c0..354c82585c 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -1,5 +1,5 @@ ## Lint the codebase. -lint: check-syntax-errors check-style lint-doc +lint: check-syntax-errors check-style lint-doc check-types @$(call print_pass,$@:) ## Compile python files to check for syntax errors. @@ -20,8 +20,12 @@ check-style: $(shell git ls-files "*.py") lint-doc: \ lint-doc-commons \ lint-doc-entities \ - lint-doc-types \ ; + @flake8 --select=D101,D102,D103,DAR \ + openfisca_core/simulations/_build_default_simulation.py \ + openfisca_core/simulations/_build_from_variables.py \ + openfisca_core/simulations/_guards.py \ + openfisca_core/simulations/_rules.py ## Run linters to check for syntax and style errors in the doc. lint-doc-%: @@ -29,7 +33,7 @@ lint-doc-%: @## @## They can be integrated into setup.cfg once all checks pass. @## The reason they're here is because otherwise we wouldn't be - @## able to integrate documentation improvements progresively. + @## able to integrate documentation improvements progressively. @## @$(call print_help,$(subst $*,%,$@:)) @flake8 --select=D101,D102,D103,DAR openfisca_core/$* @@ -39,7 +43,16 @@ lint-doc-%: ## Run static type checkers for type errors. check-types: @$(call print_help,$@:) - @mypy openfisca_core/entities openfisca_core/projectors + @mypy openfisca_core/commons + # @mypy openfisca_core/entities + # @mypy openfisca_core/projectors + @mypy \ + openfisca_core/simulations/_build_default_simulation.py \ + openfisca_core/simulations/_build_from_variables.py \ + openfisca_core/simulations/_guards.py \ + openfisca_core/simulations/_rules.py \ + openfisca_core/simulations/types.py \ + openfisca_core/types.py @$(call print_pass,$@:) ## Run code formatters to correct style errors. diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index d84956ea5c..ef5a20220a 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -37,7 +37,7 @@ test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 openfisca_core/holders \ openfisca_core/periods \ openfisca_core/projectors \ - openfisca_core/types + openfisca_core/simulations @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ coverage run -m \ ${openfisca} test $? \ diff --git a/setup.cfg b/setup.cfg index 9673496d71..d0c72f1bec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ docstring_style = google extend-ignore = D ignore = E203, E501, F405, RST301, W503 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors openfisca_core/types +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors openfisca_core/simulations max-line-length = 88 per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged @@ -59,7 +59,7 @@ skip_covered = true skip_empty = true [tool:pytest] -addopts = --doctest-modules --disable-pytest-warnings --showlocals +addopts = --disable-pytest-warnings --doctest-modules --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py testpaths = tests @@ -68,6 +68,7 @@ testpaths = tests ignore_missing_imports = True install_types = True non_interactive = True +plugins = numpy.typing.mypy_plugin [mypy-openfisca_core.commons.tests.*] ignore_errors = True diff --git a/tests/core/test_calculate_output.py b/tests/core/test_calculate_output.py index ecf59b5f7d..926db52535 100644 --- a/tests/core/test_calculate_output.py +++ b/tests/core/test_calculate_output.py @@ -2,28 +2,27 @@ from openfisca_country_template import entities, situation_examples -from openfisca_core import simulations, tools -from openfisca_core.periods import DateUnit +from openfisca_core import periods, simulations, tools from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable class simple_variable(Variable): entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH value_type = int class variable_with_calculate_output_add(Variable): entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH value_type = int calculate_output = simulations.calculate_output_add class variable_with_calculate_output_divide(Variable): entity = entities.Person - definition_period = DateUnit.YEAR + definition_period = periods.YEAR value_type = int calculate_output = simulations.calculate_output_divide diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index 8263ac3c44..bc03682460 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -2,7 +2,6 @@ from openfisca_core import periods, populations, tools from openfisca_core.errors import VariableNameConflictError, VariableNotFoundError -from openfisca_core.periods import DateUnit from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable @@ -101,7 +100,7 @@ def test_variable_with_reference(make_simulation, isolated_tax_benefit_system): assert result > 0 class disposable_income(Variable): - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(household, period): return household.empty_array() @@ -117,7 +116,7 @@ def formula(household, period): def test_variable_name_conflict(tax_benefit_system): class disposable_income(Variable): reference = "disposable_income" - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(household, period): return household.empty_array() diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index 14886532c6..0a9166d020 100644 --- a/tests/core/test_cycles.py +++ b/tests/core/test_cycles.py @@ -4,7 +4,6 @@ from openfisca_core import periods, tools from openfisca_core.errors import CycleError -from openfisca_core.periods import DateUnit from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable @@ -23,7 +22,7 @@ def simulation(tax_benefit_system): class variable1(Variable): value_type = int entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): return person("variable2", period) @@ -32,7 +31,7 @@ def formula(person, period): class variable2(Variable): value_type = int entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): return person("variable1", period) @@ -42,7 +41,7 @@ def formula(person, period): class variable3(Variable): value_type = int entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): return person("variable4", period.last_month) @@ -51,7 +50,7 @@ def formula(person, period): class variable4(Variable): value_type = int entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): return person("variable3", period) @@ -62,7 +61,7 @@ def formula(person, period): class variable5(Variable): value_type = int entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): variable6 = person("variable6", period.last_month) @@ -72,7 +71,7 @@ def formula(person, period): class variable6(Variable): value_type = int entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): variable5 = person("variable5", period) @@ -82,7 +81,7 @@ def formula(person, period): class variable7(Variable): value_type = int entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): variable5 = person("variable5", period) @@ -93,7 +92,7 @@ def formula(person, period): class cotisation(Variable): value_type = int entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): if period.start.month == 12: diff --git a/tests/core/test_formulas.py b/tests/core/test_formulas.py index c8a5379801..b9b991f652 100644 --- a/tests/core/test_formulas.py +++ b/tests/core/test_formulas.py @@ -3,8 +3,7 @@ from openfisca_country_template import entities -from openfisca_core import commons -from openfisca_core.periods import DateUnit +from openfisca_core import commons, periods from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable @@ -12,14 +11,14 @@ class choice(Variable): value_type = int entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH class uses_multiplication(Variable): value_type = int entity = entities.Person label = "Variable with formula that uses multiplication" - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): choice = person("choice", period) @@ -31,7 +30,7 @@ class returns_scalar(Variable): value_type = int entity = entities.Person label = "Variable with formula that returns a scalar value" - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): return 666 @@ -41,7 +40,7 @@ class uses_switch(Variable): value_type = int entity = entities.Person label = "Variable with formula that uses switch" - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): choice = person("choice", period) @@ -108,8 +107,8 @@ def test_group_encapsulation(): And calculations are projected to all the member families. """ + from openfisca_core import periods from openfisca_core.entities import build_entity - from openfisca_core.periods import DateUnit from openfisca_core.taxbenefitsystems import TaxBenefitSystem person_entity = build_entity( @@ -151,12 +150,12 @@ def test_group_encapsulation(): class household_level_variable(Variable): value_type = int entity = household_entity - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY class projected_family_level_variable(Variable): value_type = int entity = family_entity - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY def formula(family, period): return family.household("household_level_variable", period) diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index 088ca15935..2db121ece7 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -8,7 +8,6 @@ from openfisca_core.errors import PeriodMismatchError from openfisca_core.holders import Holder from openfisca_core.memory_config import MemoryConfig -from openfisca_core.periods import DateUnit from openfisca_core.simulations import SimulationBuilder @@ -106,9 +105,9 @@ def test_permanent_variable_filled(single): simulation = single holder = simulation.person.get_holder("birth") value = numpy.asarray(["1980-01-01"], dtype=holder.variable.dtype) - holder.set_input(periods.period(DateUnit.ETERNITY), value) + holder.set_input(periods.period(periods.ETERNITY), value) assert holder.get_array(None) == value - assert holder.get_array(DateUnit.ETERNITY) == value + assert holder.get_array(periods.ETERNITY) == value assert holder.get_array("2016-01") == value diff --git a/tests/core/test_opt_out_cache.py b/tests/core/test_opt_out_cache.py index e9fe3a2469..55ad202db4 100644 --- a/tests/core/test_opt_out_cache.py +++ b/tests/core/test_opt_out_cache.py @@ -3,7 +3,6 @@ from openfisca_country_template.entities import Person from openfisca_core import periods -from openfisca_core.periods import DateUnit from openfisca_core.variables import Variable PERIOD = periods.period("2016-01") @@ -13,14 +12,14 @@ class input(Variable): value_type = int entity = Person label = "Input variable" - definition_period = DateUnit.MONTH + definition_period = periods.MONTH class intermediate(Variable): value_type = int entity = Person label = "Intermediate result that don't need to be cached" - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): return person("input", period) @@ -30,7 +29,7 @@ class output(Variable): value_type = int entity = Person label = "Output variable" - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period): return person("intermediate", period) diff --git a/tests/core/test_projectors.py b/tests/core/test_projectors.py index 27391711c3..878b258bf1 100644 --- a/tests/core/test_projectors.py +++ b/tests/core/test_projectors.py @@ -1,8 +1,8 @@ import numpy as np +from openfisca_core import periods from openfisca_core.entities import build_entity from openfisca_core.indexed_enums import Enum -from openfisca_core.periods import DateUnit from openfisca_core.simulations.simulation_builder import SimulationBuilder from openfisca_core.taxbenefitsystems import TaxBenefitSystem from openfisca_core.variables import Variable @@ -138,14 +138,14 @@ class household_enum_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = household - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY class projected_enum_variable(Variable): value_type = Enum possible_values = enum default_value = enum.FIRST_OPTION entity = person - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY def formula(person, period): return person.household("household_enum_variable", period) @@ -209,7 +209,7 @@ class household_projected_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = household - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY def formula(household, period): return household.value_from_first_person( @@ -221,7 +221,7 @@ class person_enum_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = person - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY system.add_variables(household_projected_variable, person_enum_variable) @@ -300,14 +300,14 @@ class household_level_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = household_entity - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY class projected_family_level_variable(Variable): value_type = Enum possible_values = enum default_value = enum.FIRST_OPTION entity = family_entity - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY def formula(family, period): return family.household("household_level_variable", period) @@ -315,7 +315,7 @@ def formula(family, period): class decoded_projected_family_level_variable(Variable): value_type = str entity = family_entity - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY def formula(family, period): return family.household("household_level_variable", period).decode_to_str() diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index 0c17bb1169..1b3a1f291f 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -6,7 +6,6 @@ from openfisca_core import holders, periods, simulations from openfisca_core.parameters import ParameterNode, ValuesHistory -from openfisca_core.periods import DateUnit, Instant from openfisca_core.reforms import Reform from openfisca_core.tools import assert_near from openfisca_core.variables import Variable @@ -17,7 +16,7 @@ class goes_to_school(Variable): default_value = True entity = Person label = "The person goes to school (only relevant for children)" - definition_period = DateUnit.MONTH + definition_period = periods.MONTH class WithBasicIncomeNeutralized(Reform): @@ -319,7 +318,7 @@ class new_variable(Variable): value_type = int label = "Nouvelle variable introduite par la réforme" entity = Household - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(household, period): return household.empty_array() + 10 @@ -342,7 +341,7 @@ class new_dated_variable(Variable): value_type = int label = "Nouvelle variable introduite par la réforme" entity = Household - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula_2010_01_01(household, period): return household.empty_array() + 10 @@ -366,7 +365,7 @@ def apply(self): def test_update_variable(make_simulation, tax_benefit_system): class disposable_income(Variable): - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula_2018(household, period): return household.empty_array() + 10 @@ -403,7 +402,7 @@ def apply(self): def test_replace_variable(tax_benefit_system): class disposable_income(Variable): - definition_period = DateUnit.MONTH + definition_period = periods.MONTH entity = Person label = "Disposable income" value_type = float @@ -455,7 +454,7 @@ def apply(self): parameters_new_node = reform.parameters.children["new_node"] assert parameters_new_node is not None - instant = Instant((2013, 1, 1)) + instant = periods.Instant((2013, 1, 1)) parameters_at_instant = reform.get_parameters_at_instant(instant) assert parameters_at_instant.new_node.new_param is True @@ -465,7 +464,7 @@ class some_variable(Variable): value_type = int entity = Person label = "Variable with many attributes" - definition_period = DateUnit.MONTH + definition_period = periods.MONTH set_input = holders.set_input_divide_by_period calculate_output = simulations.calculate_output_add diff --git a/tests/core/test_simulation_builder.py b/tests/core/test_simulation_builder.py index d1dc0cde75..ecfccdcfdc 100644 --- a/tests/core/test_simulation_builder.py +++ b/tests/core/test_simulation_builder.py @@ -6,10 +6,9 @@ from openfisca_country_template import entities, situation_examples -from openfisca_core import tools +from openfisca_core import periods, tools from openfisca_core.errors import SituationParsingError from openfisca_core.indexed_enums import Enum -from openfisca_core.periods import DateUnit from openfisca_core.populations import Population from openfisca_core.simulations import Simulation, SimulationBuilder from openfisca_core.tools import test_runner @@ -19,7 +18,7 @@ @pytest.fixture def int_variable(persons): class intvar(Variable): - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY value_type = int entity = persons @@ -32,7 +31,7 @@ def __init__(self): @pytest.fixture def date_variable(persons): class datevar(Variable): - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY value_type = datetime.date entity = persons @@ -45,7 +44,7 @@ def __init__(self): @pytest.fixture def enum_variable(): class TestEnum(Variable): - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY value_type = Enum dtype = "O" default_value = "0" diff --git a/tests/core/tools/test_runner/test_yaml_runner.py b/tests/core/tools/test_runner/test_yaml_runner.py index bf04ade9bb..ac4ec00854 100644 --- a/tests/core/tools/test_runner/test_yaml_runner.py +++ b/tests/core/tools/test_runner/test_yaml_runner.py @@ -5,9 +5,8 @@ import numpy import pytest -from openfisca_core import errors +from openfisca_core import errors, periods from openfisca_core.entities import Entity -from openfisca_core.periods import DateUnit from openfisca_core.populations import Population from openfisca_core.tools.test_runner import YamlFile, YamlItem, _get_tax_benefit_system from openfisca_core.variables import Variable @@ -73,7 +72,7 @@ def __init__(self, test): class TestVariable(Variable): - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY value_type = float def __init__(self): diff --git a/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index 7bf85d9a46..39a6316bf4 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -4,7 +4,6 @@ from openfisca_country_template.entities import Person from openfisca_core import periods -from openfisca_core.periods import DateUnit from openfisca_core.variables import Variable, get_annualized_variable @@ -15,7 +14,7 @@ def monthly_variable(): class monthly_variable(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH def formula(person, period, parameters): variable.calculation_count += 1 @@ -47,7 +46,7 @@ def test_without_annualize(monthly_variable): yearly_sum = sum( person("monthly_variable", month) - for month in period.get_subperiods(DateUnit.MONTH) + for month in period.get_subperiods(periods.MONTH) ) assert monthly_variable.calculation_count == 11 @@ -62,7 +61,7 @@ def test_with_annualize(monthly_variable): yearly_sum = sum( person("monthly_variable", month) - for month in period.get_subperiods(DateUnit.MONTH) + for month in period.get_subperiods(periods.MONTH) ) assert monthly_variable.calculation_count == 0 @@ -79,7 +78,7 @@ def test_with_partial_annualize(monthly_variable): yearly_sum = sum( person("monthly_variable", month) - for month in period.get_subperiods(DateUnit.MONTH) + for month in period.get_subperiods(periods.MONTH) ) assert monthly_variable.calculation_count == 11 diff --git a/tests/core/variables/test_variables.py b/tests/core/variables/test_variables.py index 3b2790bae7..08a44a0ce8 100644 --- a/tests/core/variables/test_variables.py +++ b/tests/core/variables/test_variables.py @@ -8,7 +8,7 @@ import openfisca_country_template.situation_examples from openfisca_country_template.entities import Person -from openfisca_core.periods import DateUnit +from openfisca_core import periods from openfisca_core.simulation_builder import SimulationBuilder from openfisca_core.tools import assert_near from openfisca_core.variables import Variable @@ -67,7 +67,7 @@ def get_message(error): class variable__no_date(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable without date." @@ -88,7 +88,7 @@ def test_variable__no_date(): class variable__strange_end_attribute(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable with dubious end attribute, no formula." end = "1989-00-00" @@ -113,7 +113,7 @@ def test_variable__strange_end_attribute(): class variable__end_attribute(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable with end attribute, no formula." end = "1989-12-31" @@ -141,7 +141,7 @@ def test_variable__end_attribute_set_input(simulation): class end_attribute__one_simple_formula(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable with end attribute, one formula without date." end = "1989-12-31" @@ -187,7 +187,7 @@ def test_dates__end_attribute__one_simple_formula(): class no_end_attribute__one_formula__strange_name(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable without end attribute, one stangely named formula." def formula_2015_toto(individu, period): @@ -208,7 +208,7 @@ def test_add__no_end_attribute__one_formula__strange_name(): class no_end_attribute__one_formula__start(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable without end attribute, one dated formula." def formula_2000_01_01(individu, period): @@ -241,7 +241,7 @@ class no_end_attribute__one_formula__eternity(Variable): value_type = int entity = Person definition_period = ( - DateUnit.ETERNITY + periods.ETERNITY ) # For this entity, this variable shouldn't evolve through time label = "Variable without end attribute, one dated formula." @@ -278,7 +278,7 @@ def test_call__no_end_attribute__one_formula__eternity_after(simulation): class no_end_attribute__formulas__start_formats(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable without end attribute, multiple dated formulas." def formula_2000(individu, period): @@ -339,7 +339,7 @@ def test_call__no_end_attribute__formulas__start_formats(simulation): class no_attribute__formulas__different_names__dates_overlap(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable, no end attribute, multiple dated formulas with different names but same dates." def formula_2000(individu, period): @@ -364,7 +364,7 @@ def test_add__no_attribute__formulas__different_names__dates_overlap(): class no_attribute__formulas__different_names__no_overlap(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable, no end attribute, multiple dated formulas with different names and no date overlap." def formula_2000_01_01(individu, period): @@ -404,7 +404,7 @@ def test_call__no_attribute__formulas__different_names__no_overlap(simulation): class end_attribute__one_formula__start(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable with end attribute, one dated formula." end = "2001-12-31" @@ -432,7 +432,7 @@ def test_call__end_attribute__one_formula__start(simulation): class stop_attribute_before__one_formula__start(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable with stop attribute only coming before formula start." end = "1990-01-01" @@ -454,7 +454,7 @@ def test_add__stop_attribute_before__one_formula__start(): class end_attribute_restrictive__one_formula(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = ( "Variable with end attribute, one dated formula and dates intervals overlap." ) @@ -484,7 +484,7 @@ def test_call__end_attribute_restrictive__one_formula(simulation): class end_attribute__formulas__different_names(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "Variable with end attribute, multiple dated formulas with different names." end = "2010-12-31" @@ -535,7 +535,7 @@ def test_unexpected_attr(): class variable_with_strange_attr(Variable): value_type = int entity = Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH unexpected = "???" with raises(ValueError): diff --git a/tests/fixtures/appclient.py b/tests/fixtures/appclient.py index 5edcfc2c98..035bd6f259 100644 --- a/tests/fixtures/appclient.py +++ b/tests/fixtures/appclient.py @@ -20,7 +20,7 @@ def test_client(tax_benefit_system): class new_variable(Variable): value_type = float entity = entities.Person - definition_period = DateUnit.MONTH + definition_period = periods.MONTH label = "New variable" reference = "https://law.gov.example/new_variable" # Always use the most official source diff --git a/tests/fixtures/variables.py b/tests/fixtures/variables.py index aab7cda58d..cd0d9b70ce 100644 --- a/tests/fixtures/variables.py +++ b/tests/fixtures/variables.py @@ -1,9 +1,9 @@ -from openfisca_core.periods import DateUnit +from openfisca_core import periods from openfisca_core.variables import Variable class TestVariable(Variable): - definition_period = DateUnit.ETERNITY + definition_period = periods.ETERNITY value_type = float def __init__(self, entity):