diff --git a/CHANGELOG.md b/CHANGELOG.md
index ade8610231..6095896937 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,25 @@
# Changelog
+### 41.5.8 [#1220](https://github.com/openfisca/openfisca-core/pull/1221)
+
+#### Technical changes
+
+- Fix doc & type definitions in the projectors module
+
+### 41.5.7 [#1224](https://github.com/openfisca/openfisca-core/pull/1224)
+
+#### Technical changes
+
+- Fix doc & type definitions in the enums module
+
+### 41.5.6 [#1223](https://github.com/openfisca/openfisca-core/pull/1223)
+
+#### Technical changes
+
+- Update `pendulum'
+- Remove run-time type-checks
+- Add typing to the periods module
+
### 41.5.5 [#1220](https://github.com/openfisca/openfisca-core/pull/1220)
#### Technical changes
diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py
index bce9206938..96fd0101ed 100644
--- a/openfisca_core/commons/formulas.py
+++ b/openfisca_core/commons/formulas.py
@@ -3,14 +3,14 @@
import numpy
-from openfisca_core import types as t
+from . import types as t
def apply_thresholds(
- input: t.Array[numpy.float_],
+ input: t.Array[t.ArrayFloat],
thresholds: t.ArrayLike[float],
choices: t.ArrayLike[float],
-) -> t.Array[numpy.float_]:
+) -> t.Array[t.ArrayBool]:
"""Makes a choice based on an input and thresholds.
From a list of ``choices``, this function selects one of these values
@@ -39,7 +39,7 @@ def apply_thresholds(
"""
- condlist: list[Union[t.Array[numpy.bool_], bool]]
+ condlist: list[Union[t.Array[t.ArrayBool], bool]]
condlist = [input <= threshold for threshold in thresholds]
if len(condlist) == len(choices) - 1:
@@ -58,8 +58,8 @@ def apply_thresholds(
def concat(
- this: Union[t.Array[numpy.str_], t.ArrayLike[str]],
- that: Union[t.Array[numpy.str_], t.ArrayLike[str]],
+ this: Union[t.Array[t.ArrayStr], t.ArrayLike[str]],
+ that: Union[t.Array[t.ArrayStr], t.ArrayLike[str]],
) -> t.Array[numpy.str_]:
"""Concatenates the values of two arrays.
@@ -89,9 +89,9 @@ def concat(
def switch(
- conditions: t.Array[numpy.float_],
+ conditions: t.Array[t.ArrayFloat],
value_by_condition: Mapping[float, float],
-) -> t.Array[numpy.float_]:
+) -> t.Array[t.ArrayFloat]:
"""Mimicks a switch statement.
Given an array of conditions, returns an array of the same size,
diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py
index 342bbbe5fb..ae66bec33d 100644
--- a/openfisca_core/commons/misc.py
+++ b/openfisca_core/commons/misc.py
@@ -1,7 +1,5 @@
from typing import Optional, TypeVar
-import numpy
-
from openfisca_core import types as t
T = TypeVar("T")
@@ -45,7 +43,7 @@ def empty_clone(original: T) -> T:
return new
-def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str:
+def stringify_array(array: Optional[t.Array[t.ArrayAny]]) -> 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 6df1f1fee2..ca34c06479 100644
--- a/openfisca_core/commons/rates.py
+++ b/openfisca_core/commons/rates.py
@@ -1,15 +1,15 @@
from typing import Optional
-from openfisca_core.types import Array, ArrayLike
-
import numpy
+from . import types as t
+
def average_rate(
- target: Array[numpy.float_],
- varying: ArrayLike[float],
- trim: Optional[ArrayLike[float]] = None,
-) -> Array[numpy.float_]:
+ target: t.Array[t.ArrayFloat],
+ varying: t.ArrayLike[float],
+ trim: Optional[t.ArrayLike[float]] = None,
+) -> t.Array[t.ArrayFloat]:
"""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[numpy.float_]
+ average_rate: t.Array[t.ArrayFloat]
average_rate = 1 - target / varying
@@ -62,10 +62,10 @@ def average_rate(
def marginal_rate(
- target: Array[numpy.float_],
- varying: Array[numpy.float_],
- trim: Optional[ArrayLike[float]] = None,
-) -> Array[numpy.float_]:
+ target: t.Array[t.ArrayFloat],
+ varying: t.Array[t.ArrayFloat],
+ trim: Optional[t.ArrayLike[float]] = None,
+) -> t.Array[t.ArrayFloat]:
"""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[numpy.float_]
+ marginal_rate: t.Array[t.ArrayFloat]
marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:])
diff --git a/openfisca_core/commons/types.py b/openfisca_core/commons/types.py
new file mode 100644
index 0000000000..8d02a39f44
--- /dev/null
+++ b/openfisca_core/commons/types.py
@@ -0,0 +1,6 @@
+from openfisca_core.types import Array as Array # noqa: F401
+from openfisca_core.types import ArrayAny as ArrayAny # noqa: F401
+from openfisca_core.types import ArrayBool as ArrayBool # noqa: F401
+from openfisca_core.types import ArrayFloat as ArrayFloat # noqa: F401
+from openfisca_core.types import ArrayLike as ArrayLike # noqa: F401
+from openfisca_core.types import ArrayStr as ArrayStr # noqa: F401
diff --git a/openfisca_core/indexed_enums/__init__.py b/openfisca_core/indexed_enums/__init__.py
index 874a7e1f9b..03958426b9 100644
--- a/openfisca_core/indexed_enums/__init__.py
+++ b/openfisca_core/indexed_enums/__init__.py
@@ -21,6 +21,7 @@
#
# See: https://www.python.org/dev/peps/pep-0008/#imports
-from .config import ENUM_ARRAY_DTYPE # noqa: F401
-from .enum import Enum # noqa: F401
-from .enum_array import EnumArray # noqa: F401
+from .enum import Enum
+from .enum_array import EnumArray
+
+__all__ = ["Enum", "EnumArray"]
diff --git a/openfisca_core/indexed_enums/config.py b/openfisca_core/indexed_enums/config.py
deleted file mode 100644
index f7da69b847..0000000000
--- a/openfisca_core/indexed_enums/config.py
+++ /dev/null
@@ -1,3 +0,0 @@
-import numpy
-
-ENUM_ARRAY_DTYPE = numpy.int16
diff --git a/openfisca_core/indexed_enums/enum.py b/openfisca_core/indexed_enums/enum.py
index 7957ced3a2..4869e5ba5b 100644
--- a/openfisca_core/indexed_enums/enum.py
+++ b/openfisca_core/indexed_enums/enum.py
@@ -1,16 +1,12 @@
from __future__ import annotations
-from typing import Union
-
-import enum
-
import numpy
-from .config import ENUM_ARRAY_DTYPE
+from . import types as t
from .enum_array import EnumArray
-class Enum(enum.Enum):
+class Enum(t.Enum):
"""
Enum based on `enum34 `_, whose items
have an index.
@@ -21,7 +17,7 @@ def __init__(self, name: str) -> None:
# When the enum item is initialized, self._member_names_ contains the
# names of the previously initialized items, so its length is the index
# of this item.
- self.index = len(self._member_names_)
+ self.index = len(self._member_names_) # type: ignore[attr-defined]
# Bypass the slow Enum.__eq__
__eq__ = object.__eq__
@@ -33,13 +29,14 @@ def __init__(self, name: str) -> None:
@classmethod
def encode(
cls,
- array: Union[
- EnumArray,
- numpy.int_,
- numpy.float_,
- numpy.object_,
- ],
- ) -> EnumArray:
+ array: (
+ t.EnumArray
+ | t.Array[t.ArrayBytes]
+ | t.Array[t.ArrayEnum]
+ | t.Array[t.ArrayInt]
+ | t.Array[t.ArrayStr]
+ ),
+ ) -> t.EnumArray:
"""
Encode a string numpy array, an enum item numpy array, or an int numpy
array into an :any:`EnumArray`. See :any:`EnumArray.decode` for
@@ -65,6 +62,7 @@ def encode(
>>> encoded_array[0]
2 # Encoded value
"""
+
if isinstance(array, EnumArray):
return array
@@ -73,7 +71,7 @@ def encode(
array = numpy.select(
[array == item.name for item in cls],
[item.index for item in cls],
- ).astype(ENUM_ARRAY_DTYPE)
+ ).astype(t.ArrayEnum)
# Enum items arrays
elif isinstance(array, numpy.ndarray) and array.dtype.kind == "O":
@@ -94,6 +92,7 @@ def encode(
array = numpy.select(
[array == item for item in cls],
[item.index for item in cls],
- ).astype(ENUM_ARRAY_DTYPE)
+ ).astype(t.ArrayEnum)
+ array = numpy.asarray(array, dtype=t.ArrayEnum)
return EnumArray(array, cls)
diff --git a/openfisca_core/indexed_enums/enum_array.py b/openfisca_core/indexed_enums/enum_array.py
index 2742719ada..de6f83df4c 100644
--- a/openfisca_core/indexed_enums/enum_array.py
+++ b/openfisca_core/indexed_enums/enum_array.py
@@ -1,15 +1,14 @@
from __future__ import annotations
-import typing
-from typing import Any, NoReturn, Optional, Type
+from typing import NoReturn, overload
+from typing_extensions import TypeGuard
import numpy
-if typing.TYPE_CHECKING:
- from openfisca_core.indexed_enums import Enum
+from . import types as t
-class EnumArray(numpy.ndarray):
+class EnumArray(t.EnumArray):
"""
NumPy array subclass representing an array of enum items.
@@ -19,51 +18,86 @@ class EnumArray(numpy.ndarray):
# Subclassing ndarray is a little tricky.
# To read more about the two following methods, see:
# https://docs.scipy.org/doc/numpy-1.13.0/user/basics.subclassing.html#slightly-more-realistic-example-attribute-added-to-existing-array.
+
+ #: Enum type of the array items.
+ possible_values: None | type[t.Enum] = None
+
def __new__(
cls,
- input_array: numpy.int_,
- possible_values: Optional[Type[Enum]] = None,
+ input_array: t.Array[t.ArrayEnum],
+ possible_values: None | type[t.Enum] = None,
) -> EnumArray:
obj = numpy.asarray(input_array).view(cls)
obj.possible_values = possible_values
return obj
# See previous comment
- def __array_finalize__(self, obj: Optional[numpy.int_]) -> None:
+ def __array_finalize__(self, obj: None | t.EnumArray | t.Array[t.ArrayAny]) -> None:
if obj is None:
- return
+ return None
+ if isinstance(obj, EnumArray):
+ self.possible_values = obj.possible_values
+ return None
+
+ @overload # type: ignore[override]
+ def __eq__(self, other: None | t.Enum | type[t.Enum]) -> t.Array[t.ArrayBool]:
+ ...
+
+ @overload
+ def __eq__(self, other: object) -> t.Array[t.ArrayBool] | bool:
+ ...
+
+ def __eq__(self, other: object) -> t.Array[t.ArrayBool] | bool:
+ boolean_array: t.Array[t.ArrayBool]
+ boolean: bool
- self.possible_values = getattr(obj, "possible_values", None)
+ if self.possible_values is None:
+ return NotImplemented
+
+ view: t.Array[t.ArrayEnum] = self.view(numpy.ndarray)
+
+ if other is None or self._is_an_enum_type(other):
+ boolean_array = view == other
+ return boolean_array
- def __eq__(self, other: Any) -> bool:
# When comparing to an item of self.possible_values, use the item index
# to speed up the comparison.
- if other.__class__.__name__ is self.possible_values.__name__:
+ if self._is_an_enum(other):
# Use view(ndarray) so that the result is a classic ndarray, not an
# EnumArray.
- return self.view(numpy.ndarray) == other.index
+ boolean_array = view == other.index
+ return boolean_array
+
+ boolean = view == other
+ return boolean
- return self.view(numpy.ndarray) == other
+ @overload # type: ignore[override]
+ def __ne__(self, other: None | t.Enum | type[t.Enum]) -> t.Array[t.ArrayBool]:
+ ...
- def __ne__(self, other: Any) -> bool:
+ @overload
+ def __ne__(self, other: object) -> t.Array[t.ArrayBool] | bool:
+ ...
+
+ def __ne__(self, other: object) -> t.Array[t.ArrayBool] | bool:
return numpy.logical_not(self == other)
- def _forbidden_operation(self, other: Any) -> NoReturn:
+ def _forbidden_operation(self, other: object) -> NoReturn:
raise TypeError(
"Forbidden operation. The only operations allowed on EnumArrays "
"are '==' and '!='.",
)
- __add__ = _forbidden_operation
- __mul__ = _forbidden_operation
- __lt__ = _forbidden_operation
- __le__ = _forbidden_operation
- __gt__ = _forbidden_operation
- __ge__ = _forbidden_operation
- __and__ = _forbidden_operation
- __or__ = _forbidden_operation
+ __add__ = _forbidden_operation # type: ignore[assignment]
+ __mul__ = _forbidden_operation # type: ignore[assignment]
+ __lt__ = _forbidden_operation # type: ignore[assignment]
+ __le__ = _forbidden_operation # type: ignore[assignment]
+ __gt__ = _forbidden_operation # type: ignore[assignment]
+ __ge__ = _forbidden_operation # type: ignore[assignment]
+ __and__ = _forbidden_operation # type: ignore[assignment]
+ __or__ = _forbidden_operation # type: ignore[assignment]
- def decode(self) -> numpy.object_:
+ def decode(self) -> t.Array[t.ArrayEnum]:
"""
Return the array of enum items corresponding to self.
@@ -77,12 +111,16 @@ def decode(self) -> numpy.object_:
Decoded value: enum item
"""
+
+ if self.possible_values is None:
+ raise NotImplementedError
+
return numpy.select(
[self == item.index for item in self.possible_values],
- list(self.possible_values),
+ [item for item in self.possible_values], # type: ignore[misc]
)
- def decode_to_str(self) -> numpy.str_:
+ def decode_to_str(self) -> t.Array[t.ArrayStr]:
"""
Return the array of string identifiers corresponding to self.
@@ -94,6 +132,10 @@ def decode_to_str(self) -> numpy.str_:
>>> enum_array.decode_to_str()[0]
'free_lodger' # String identifier
"""
+
+ if self.possible_values is None:
+ raise NotImplementedError
+
return numpy.select(
[self == item.index for item in self.possible_values],
[item.name for item in self.possible_values],
@@ -104,3 +146,21 @@ def __repr__(self) -> str:
def __str__(self) -> str:
return str(self.decode_to_str())
+
+ def _is_an_enum(self, other: object) -> TypeGuard[t.Enum]:
+ if self.possible_values is None:
+ raise NotImplementedError
+
+ return (
+ not hasattr(other, "__name__")
+ and other.__class__.__name__ is self.possible_values.__name__
+ )
+
+ def _is_an_enum_type(self, other: object) -> TypeGuard[type[t.Enum]]:
+ if self.possible_values is None:
+ raise NotImplementedError
+
+ return (
+ hasattr(other, "__name__")
+ and other.__name__ is self.possible_values.__name__
+ )
diff --git a/openfisca_core/indexed_enums/py.typed b/openfisca_core/indexed_enums/py.typed
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openfisca_core/indexed_enums/types.py b/openfisca_core/indexed_enums/types.py
new file mode 100644
index 0000000000..57657b8c56
--- /dev/null
+++ b/openfisca_core/indexed_enums/types.py
@@ -0,0 +1,22 @@
+from __future__ import annotations
+
+from openfisca_core.types import Array as Array
+from openfisca_core.types import ArrayAny as ArrayAny # noqa: F401
+from openfisca_core.types import ArrayBool as ArrayBool # noqa: F401
+from openfisca_core.types import ArrayBytes as ArrayBytes # noqa: F401
+from openfisca_core.types import ArrayEnum as ArrayEnum
+from openfisca_core.types import ArrayInt as ArrayInt # noqa: F401
+from openfisca_core.types import ArrayStr as ArrayStr # noqa: F401
+
+import abc
+import enum
+
+# Indexed enums
+
+
+class Enum(enum.Enum):
+ index: int
+
+
+class EnumArray(Array[ArrayEnum], metaclass=abc.ABCMeta):
+ ...
diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py
index 4669c7ff4f..1f2e68acaf 100644
--- a/openfisca_core/periods/__init__.py
+++ b/openfisca_core/periods/__init__.py
@@ -21,20 +21,15 @@
#
# See: https://www.python.org/dev/peps/pep-0008/#imports
-from .config import ( # noqa: F401
- DAY,
- ETERNITY,
+from . import types
+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 +37,39 @@
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",
+ "types",
+]
diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py
index 64b2077831..0342cc71ed 100644
--- a/openfisca_core/periods/_parsers.py
+++ b/openfisca_core/periods/_parsers.py
@@ -6,6 +6,7 @@
from pendulum.datetime import Date
from pendulum.parsing import ParserError
+from . import types as t
from .date_unit import DateUnit
from .instant_ import Instant
from .period_ import Period
@@ -13,7 +14,7 @@
invalid_week = re.compile(r".*(W[1-9]|W[1-9]-[0-9]|W[0-5][0-9]-0)$")
-def _parse_period(value: str) -> Optional[Period]:
+def _parse_period(value: str) -> Optional[t.Period]:
"""Parses ISO format/calendar periods.
Such as "2012" or "2015-03".
@@ -57,7 +58,7 @@ def _parse_period(value: str) -> Optional[Period]:
return Period((unit, instant, 1))
-def _parse_unit(value: str) -> DateUnit:
+def _parse_unit(value: str) -> t.DateUnit:
"""Determine the date unit of a date string.
Args:
diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/config.py
index 17807160e4..3b4c0a7eb0 100644
--- a/openfisca_core/periods/config.py
+++ b/openfisca_core/periods/config.py
@@ -1,13 +1,8 @@
import re
-from .date_unit import DateUnit
+from pendulum.datetime import Date
-WEEKDAY = DateUnit.WEEKDAY
-WEEK = DateUnit.WEEK
-DAY = DateUnit.DAY
-MONTH = DateUnit.MONTH
-YEAR = DateUnit.YEAR
-ETERNITY = DateUnit.ETERNITY
+from . import types as t
# Matches "2015", "2015-01", "2015-01-01"
# Does not match "2015-13", "2015-12-32"
@@ -15,8 +10,8 @@
r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$"
)
-date_by_instant_cache: dict = {}
-str_by_instant_cache: dict = {}
+date_by_instant_cache: dict[t.Instant, Date] = {}
+str_by_instant_cache: dict[t.Instant, t.InstantStr] = {}
year_or_month_or_day_re = re.compile(
r"(18|19|20)\d{2}(-(0?[1-9]|1[0-2])(-([0-2]?\d|3[0-1]))?)?$"
)
diff --git a/openfisca_core/periods/date_unit.py b/openfisca_core/periods/date_unit.py
index a813211495..13d93f8a24 100644
--- a/openfisca_core/periods/date_unit.py
+++ b/openfisca_core/periods/date_unit.py
@@ -4,10 +4,12 @@
from strenum import StrEnum
+from . import types as t
+
class DateUnitMeta(EnumMeta):
@property
- def isoformat(self) -> tuple[DateUnit, ...]:
+ def isoformat(self) -> tuple[t.DateUnit, ...]:
"""Creates a :obj:`tuple` of ``key`` with isoformat items.
Returns:
@@ -28,7 +30,7 @@ def isoformat(self) -> tuple[DateUnit, ...]:
return DateUnit.DAY, DateUnit.MONTH, DateUnit.YEAR
@property
- def isocalendar(self) -> tuple[DateUnit, ...]:
+ def isocalendar(self) -> tuple[t.DateUnit, ...]:
"""Creates a :obj:`tuple` of ``key`` with isocalendar items.
Returns:
@@ -96,6 +98,11 @@ class DateUnit(StrEnum, metaclass=DateUnitMeta):
"""
+ def __contains__(self, other: object) -> bool:
+ if isinstance(other, str):
+ return super().__contains__(other)
+ return NotImplemented
+
WEEKDAY = "weekday"
WEEK = "week"
DAY = "day"
diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py
index 2ce4e0cd35..ce27c54d0d 100644
--- a/openfisca_core/periods/helpers.py
+++ b/openfisca_core/periods/helpers.py
@@ -1,4 +1,6 @@
-from typing import NoReturn, Optional
+from __future__ import annotations
+
+from typing import NoReturn, Optional, overload
import datetime
import os
@@ -7,12 +9,23 @@
from pendulum.parsing import ParserError
from . import _parsers, config
+from . import types as t
from .date_unit import DateUnit
from .instant_ import Instant
from .period_ import Period
-def instant(instant) -> Optional[Instant]:
+@overload
+def instant(instant: None) -> None:
+ ...
+
+
+@overload
+def instant(instant: object) -> Instant:
+ ...
+
+
+def instant(instant: object | None) -> t.Instant | None:
"""Build a new instant, aka a triple of integers (year, month, day).
Args:
@@ -49,6 +62,8 @@ def instant(instant) -> Optional[Instant]:
"""
+ result: t.Instant | tuple[int, ...]
+
if instant is None:
return None
if isinstance(instant, Instant):
@@ -58,27 +73,28 @@ def instant(instant) -> Optional[Instant]:
raise ValueError(
f"'{instant}' is not a valid instant. Instants are described using the 'YYYY-MM-DD' format, for instance '2015-06-15'."
)
- instant = Instant(int(fragment) for fragment in instant.split("-", 2)[:3])
+ result = Instant(int(fragment) for fragment in instant.split("-", 2)[:3])
elif isinstance(instant, datetime.date):
- instant = Instant((instant.year, instant.month, instant.day))
+ result = Instant((instant.year, instant.month, instant.day))
elif isinstance(instant, int):
- instant = (instant,)
+ result = (instant,)
elif isinstance(instant, list):
assert 1 <= len(instant) <= 3
- instant = tuple(instant)
+ result = tuple(instant)
elif isinstance(instant, Period):
- instant = instant.start
+ result = instant.start
else:
assert isinstance(instant, tuple), instant
assert 1 <= len(instant) <= 3
- if len(instant) == 1:
- return Instant((instant[0], 1, 1))
- if len(instant) == 2:
- return Instant((instant[0], instant[1], 1))
- return Instant(instant)
+ result = instant
+ if len(result) == 1:
+ return Instant((result[0], 1, 1))
+ if len(result) == 2:
+ return Instant((result[0], result[1], 1))
+ return Instant(result)
-def instant_date(instant: Optional[Instant]) -> Optional[datetime.date]:
+def instant_date(instant: Optional[t.Instant]) -> Optional[datetime.date]:
"""Returns the date representation of an :class:`.Instant`.
Args:
@@ -105,7 +121,7 @@ def instant_date(instant: Optional[Instant]) -> Optional[datetime.date]:
return instant_date
-def period(value) -> Period:
+def period(value: object) -> t.Period:
"""Build a new period, aka a triple (unit, start_instant, size).
Args:
@@ -125,7 +141,7 @@ def period(value) -> Period:
Period((, Instant((2021, 1, 1)), 1))
>>> period(DateUnit.ETERNITY)
- Period((, Instant((1, 1, 1)), inf))
+ Period((, Instant((1, 1, 1)), 0))
>>> period(2021)
Period((, Instant((2021, 1, 1)), 1))
@@ -164,15 +180,9 @@ def period(value) -> Period:
return Period((DateUnit.DAY, instant(value), 1))
# We return an "eternity-period", for example
- # ``, inf))>``.
+ # ``, 0))>``.
if str(value).lower() == DateUnit.ETERNITY:
- return Period(
- (
- DateUnit.ETERNITY,
- instant(datetime.date.min),
- float("inf"),
- )
- )
+ return Period.eternity()
# For example ``2021`` gives
# ``, 1))>``.
@@ -181,7 +191,7 @@ def period(value) -> Period:
# Up to this point, if ``value`` is not a :obj:`str`, we desist.
if not isinstance(value, str):
- _raise_error(value)
+ _raise_error(str(value))
# There can't be empty strings.
if not value:
@@ -267,7 +277,7 @@ def _raise_error(value: str) -> NoReturn:
raise ValueError(message)
-def key_period_size(period: Period) -> str:
+def key_period_size(period: t.Period) -> str:
"""Define a key in order to sort periods by length.
It uses two aspects: first, ``unit``, then, ``size``.
@@ -291,12 +301,10 @@ def key_period_size(period: Period) -> str:
"""
- unit, start, size = period
-
- return f"{unit_weight(unit)}_{size}"
+ return f"{unit_weight(period.unit)}_{period.size}"
-def unit_weights() -> dict[str, int]:
+def unit_weights() -> dict[t.DateUnit, int]:
"""Assign weights to date units.
Examples:
@@ -315,7 +323,7 @@ def unit_weights() -> dict[str, int]:
}
-def unit_weight(unit: str) -> int:
+def unit_weight(unit: t.DateUnit) -> int:
"""Retrieves a specific date unit weight.
Examples:
diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py
index 9d0893ba41..5eed50c452 100644
--- a/openfisca_core/periods/instant_.py
+++ b/openfisca_core/periods/instant_.py
@@ -1,10 +1,14 @@
+from __future__ import annotations
+
import pendulum
+from pendulum.datetime import Date
from . import config
+from . import types as t
from .date_unit import DateUnit
-class Instant(tuple):
+class Instant(tuple[int, int, int]):
"""An instant in time (year, month, day).
An :class:`.Instant` represents the most atomic and indivisible
@@ -13,10 +17,6 @@ class Instant(tuple):
Current implementation considers this unit to be a day, so
:obj:`instants <.Instant>` can be thought of as "day dates".
- Args:
- (tuple(tuple(int, int, int))):
- The ``year``, ``month``, and ``day``, accordingly.
-
Examples:
>>> instant = Instant((2021, 9, 13))
@@ -78,35 +78,51 @@ class Instant(tuple):
"""
- def __repr__(self):
+ def __repr__(self) -> str:
return f"{self.__class__.__name__}({super().__repr__()})"
- def __str__(self):
+ def __str__(self) -> t.InstantStr:
instant_str = config.str_by_instant_cache.get(self)
if instant_str is None:
- config.str_by_instant_cache[self] = instant_str = self.date.isoformat()
+ instant_str = t.InstantStr(self.date.isoformat())
+ config.str_by_instant_cache[self] = instant_str
return instant_str
+ def __lt__(self, other: object) -> bool:
+ if isinstance(other, Instant):
+ return super().__lt__(other)
+ return NotImplemented
+
+ def __le__(self, other: object) -> bool:
+ if isinstance(other, Instant):
+ return super().__le__(other)
+ return NotImplemented
+
@property
- def date(self):
+ def date(self) -> Date:
instant_date = config.date_by_instant_cache.get(self)
if instant_date is None:
- config.date_by_instant_cache[self] = instant_date = pendulum.date(*self)
+ instant_date = pendulum.date(*self)
+ config.date_by_instant_cache[self] = instant_date
return instant_date
@property
- def day(self):
+ def day(self) -> int:
return self[2]
@property
- def month(self):
+ def month(self) -> int:
return self[1]
- def offset(self, offset, unit):
+ @property
+ def year(self) -> int:
+ return self[0]
+
+ def offset(self, offset: str | int, unit: t.DateUnit) -> t.Instant | None:
"""Increments/decrements the given instant with offset units.
Args:
@@ -193,6 +209,9 @@ def offset(self, offset, unit):
date = date.add(days=offset)
return self.__class__((date.year, date.month, date.day))
- @property
- def year(self):
- return self[0]
+ return None
+
+ @classmethod
+ def eternity(cls) -> t.Instant:
+ """Return an eternity instant."""
+ return cls((1, 1, 1))
diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py
index 11a7b671b4..39884d9002 100644
--- a/openfisca_core/periods/period_.py
+++ b/openfisca_core/periods/period_.py
@@ -1,22 +1,19 @@
from __future__ import annotations
-import typing
from collections.abc import Sequence
import calendar
import datetime
-import pendulum
+from pendulum.datetime import Date
from . import helpers
+from . import types as t
from .date_unit import DateUnit
from .instant_ import Instant
-if typing.TYPE_CHECKING:
- from pendulum.datetime import Date
-
-class Period(tuple):
+class Period(tuple[t.DateUnit, t.Instant, int]):
"""Toolbox to handle date intervals.
A :class:`.Period` is a triple (``unit``, ``start``, ``size``).
@@ -124,11 +121,11 @@ class Period(tuple):
def __repr__(self) -> str:
return f"{self.__class__.__name__}({super().__repr__()})"
- def __str__(self) -> str:
+ def __str__(self) -> t.PeriodStr:
unit, start_instant, size = self
if unit == DateUnit.ETERNITY:
- return unit.upper()
+ return t.PeriodStr(unit.upper())
# ISO format date units.
f_year, month, day = start_instant
@@ -140,58 +137,58 @@ def __str__(self) -> str:
if unit == DateUnit.MONTH and size == 12 or unit == DateUnit.YEAR and size == 1:
if month == 1:
# civil year starting from january
- return str(f_year)
+ return t.PeriodStr(str(f_year))
else:
# rolling year
- return f"{DateUnit.YEAR}:{f_year}-{month:02d}"
+ return t.PeriodStr(f"{DateUnit.YEAR}:{f_year}-{month:02d}")
# simple month
if unit == DateUnit.MONTH and size == 1:
- return f"{f_year}-{month:02d}"
+ return t.PeriodStr(f"{f_year}-{month:02d}")
# several civil years
if unit == DateUnit.YEAR and month == 1:
- return f"{unit}:{f_year}:{size}"
+ return t.PeriodStr(f"{unit}:{f_year}:{size}")
if unit == DateUnit.DAY:
if size == 1:
- return f"{f_year}-{month:02d}-{day:02d}"
+ return t.PeriodStr(f"{f_year}-{month:02d}-{day:02d}")
else:
- return f"{unit}:{f_year}-{month:02d}-{day:02d}:{size}"
+ return t.PeriodStr(f"{unit}:{f_year}-{month:02d}-{day:02d}:{size}")
# 1 week
if unit == DateUnit.WEEK and size == 1:
if week < 10:
- return f"{c_year}-W0{week}"
+ return t.PeriodStr(f"{c_year}-W0{week}")
- return f"{c_year}-W{week}"
+ return t.PeriodStr(f"{c_year}-W{week}")
# several weeks
if unit == DateUnit.WEEK and size > 1:
if week < 10:
- return f"{unit}:{c_year}-W0{week}:{size}"
+ return t.PeriodStr(f"{unit}:{c_year}-W0{week}:{size}")
- return f"{unit}:{c_year}-W{week}:{size}"
+ return t.PeriodStr(f"{unit}:{c_year}-W{week}:{size}")
# 1 weekday
if unit == DateUnit.WEEKDAY and size == 1:
if week < 10:
- return f"{c_year}-W0{week}-{weekday}"
+ return t.PeriodStr(f"{c_year}-W0{week}-{weekday}")
- return f"{c_year}-W{week}-{weekday}"
+ return t.PeriodStr(f"{c_year}-W{week}-{weekday}")
# several weekdays
if unit == DateUnit.WEEKDAY and size > 1:
if week < 10:
- return f"{unit}:{c_year}-W0{week}-{weekday}:{size}"
+ return t.PeriodStr(f"{unit}:{c_year}-W0{week}-{weekday}:{size}")
- return f"{unit}:{c_year}-W{week}-{weekday}:{size}"
+ return t.PeriodStr(f"{unit}:{c_year}-W{week}-{weekday}:{size}")
# complex period
- return f"{unit}:{f_year}-{month:02d}:{size}"
+ return t.PeriodStr(f"{unit}:{f_year}-{month:02d}:{size}")
@property
- def unit(self) -> str:
+ def unit(self) -> t.DateUnit:
"""The ``unit`` of the ``Period``.
Example:
@@ -205,7 +202,7 @@ def unit(self) -> str:
return self[0]
@property
- def start(self) -> Instant:
+ def start(self) -> t.Instant:
"""The ``Instant`` at which the ``Period`` starts.
Example:
@@ -322,7 +319,12 @@ def size_in_days(self) -> int:
"""
if self.unit in (DateUnit.YEAR, DateUnit.MONTH):
- last_day = self.start.offset(self.size, self.unit).offset(-1, DateUnit.DAY)
+ last = self.start.offset(self.size, self.unit)
+ if last is None:
+ raise NotImplementedError
+ last_day = last.offset(-1, DateUnit.DAY)
+ if last_day is None:
+ raise NotImplementedError
return (last_day.date - self.start.date).days + 1
if self.unit == DateUnit.WEEK:
@@ -334,7 +336,7 @@ def size_in_days(self) -> int:
raise ValueError(f"Can't calculate number of days in a {self.unit}.")
@property
- def size_in_weeks(self):
+ def size_in_weeks(self) -> int:
"""The ``size`` of the ``Period`` in weeks.
Examples:
@@ -353,14 +355,14 @@ def size_in_weeks(self):
if self.unit == DateUnit.YEAR:
start = self.start.date
cease = start.add(years=self.size)
- delta = pendulum.period(start, cease)
- return delta.as_interval().weeks
+ delta = start.diff(cease)
+ return delta.in_weeks()
if self.unit == DateUnit.MONTH:
start = self.start.date
cease = start.add(months=self.size)
- delta = pendulum.period(start, cease)
- return delta.as_interval().weeks
+ delta = start.diff(cease)
+ return delta.in_weeks()
if self.unit == DateUnit.WEEK:
return self.size
@@ -368,7 +370,7 @@ def size_in_weeks(self):
raise ValueError(f"Can't calculate number of weeks in a {self.unit}.")
@property
- def size_in_weekdays(self):
+ def size_in_weekdays(self) -> int:
"""The ``size`` of the ``Period`` in weekdays.
Examples:
@@ -387,8 +389,13 @@ def size_in_weekdays(self):
if self.unit == DateUnit.YEAR:
return self.size_in_weeks * 7
- if self.unit in DateUnit.MONTH:
- last_day = self.start.offset(self.size, self.unit).offset(-1, DateUnit.DAY)
+ if DateUnit.MONTH in self.unit:
+ last = self.start.offset(self.size, self.unit)
+ if last is None:
+ raise NotImplementedError
+ last_day = last.offset(-1, DateUnit.DAY)
+ if last_day is None:
+ raise NotImplementedError
return (last_day.date - self.start.date).days + 1
if self.unit == DateUnit.WEEK:
@@ -400,11 +407,13 @@ def size_in_weekdays(self):
raise ValueError(f"Can't calculate number of weekdays in a {self.unit}.")
@property
- def days(self):
+ def days(self) -> int:
"""Same as ``size_in_days``."""
return (self.stop.date - self.start.date).days + 1
- def intersection(self, start, stop):
+ def intersection(
+ self, start: t.Instant | None, stop: t.Instant | None
+ ) -> t.Period | None:
if start is None and stop is None:
return self
period_start = self[1]
@@ -457,7 +466,7 @@ def intersection(self, start, stop):
)
)
- def get_subperiods(self, unit: DateUnit) -> Sequence[Period]:
+ def get_subperiods(self, unit: t.DateUnit) -> Sequence[t.Period]:
"""Return the list of periods of unit ``unit`` contained in self.
Examples:
@@ -502,7 +511,7 @@ def get_subperiods(self, unit: DateUnit) -> Sequence[Period]:
raise ValueError(f"Cannot subdivide {self.unit} into {unit}")
- def offset(self, offset, unit=None):
+ def offset(self, offset: str | int, unit: t.DateUnit | None = None) -> t.Period:
"""Increment (or decrement) the given period with offset units.
Examples:
@@ -676,15 +685,22 @@ def offset(self, offset, unit=None):
"""
+ start: None | t.Instant = self[1].offset(
+ offset, self[0] if unit is None else unit
+ )
+
+ if start is None:
+ raise NotImplementedError
+
return self.__class__(
(
self[0],
- self[1].offset(offset, self[0] if unit is None else unit),
+ start,
self[2],
)
)
- def contains(self, other: Period) -> bool:
+ def contains(self, other: t.Period) -> bool:
"""Returns ``True`` if the period contains ``other``.
For instance, ``period(2015)`` contains ``period(2015-01)``.
@@ -694,7 +710,7 @@ def contains(self, other: Period) -> bool:
return self.start <= other.start and self.stop >= other.stop
@property
- def stop(self) -> Instant:
+ def stop(self) -> t.Instant:
"""Return the last day of the period as an Instant instance.
Examples:
@@ -731,7 +747,7 @@ def stop(self) -> Instant:
year, month, day = start_instant
if unit == DateUnit.ETERNITY:
- return Instant((float("inf"), float("inf"), float("inf")))
+ return Instant.eternity()
elif unit == DateUnit.YEAR:
date = start_instant.date.add(years=size, days=-1)
@@ -755,67 +771,82 @@ def stop(self) -> Instant:
# Reference periods
@property
- def last_week(self) -> Period:
+ def last_week(self) -> t.Period:
return self.first_week.offset(-1)
@property
- def last_fortnight(self) -> Period:
- start: Instant = self.first_week.start
+ def last_fortnight(self) -> t.Period:
+ start: t.Instant = self.first_week.start
return self.__class__((DateUnit.WEEK, start, 1)).offset(-2)
@property
- def last_2_weeks(self) -> Period:
- start: Instant = self.first_week.start
+ def last_2_weeks(self) -> t.Period:
+ start: t.Instant = self.first_week.start
return self.__class__((DateUnit.WEEK, start, 2)).offset(-2)
@property
- def last_26_weeks(self) -> Period:
- start: Instant = self.first_week.start
+ def last_26_weeks(self) -> t.Period:
+ start: t.Instant = self.first_week.start
return self.__class__((DateUnit.WEEK, start, 26)).offset(-26)
@property
- def last_52_weeks(self) -> Period:
- start: Instant = self.first_week.start
+ def last_52_weeks(self) -> t.Period:
+ start: t.Instant = self.first_week.start
return self.__class__((DateUnit.WEEK, start, 52)).offset(-52)
@property
- def last_month(self) -> Period:
+ def last_month(self) -> t.Period:
return self.first_month.offset(-1)
@property
- def last_3_months(self) -> Period:
- start: Instant = self.first_month.start
+ def last_3_months(self) -> t.Period:
+ start: t.Instant = self.first_month.start
return self.__class__((DateUnit.MONTH, start, 3)).offset(-3)
@property
- def last_year(self) -> Period:
- start: Instant = self.start.offset("first-of", DateUnit.YEAR)
+ def last_year(self) -> t.Period:
+ start: None | t.Instant = self.start.offset("first-of", DateUnit.YEAR)
+ if start is None:
+ raise NotImplementedError
return self.__class__((DateUnit.YEAR, start, 1)).offset(-1)
@property
- def n_2(self) -> Period:
- start: Instant = self.start.offset("first-of", DateUnit.YEAR)
+ def n_2(self) -> t.Period:
+ start: None | t.Instant = self.start.offset("first-of", DateUnit.YEAR)
+ if start is None:
+ raise NotImplementedError
return self.__class__((DateUnit.YEAR, start, 1)).offset(-2)
@property
- def this_year(self) -> Period:
- start: Instant = self.start.offset("first-of", DateUnit.YEAR)
+ def this_year(self) -> t.Period:
+ start: None | t.Instant = self.start.offset("first-of", DateUnit.YEAR)
+ if start is None:
+ raise NotImplementedError
return self.__class__((DateUnit.YEAR, start, 1))
@property
- def first_month(self) -> Period:
- start: Instant = self.start.offset("first-of", DateUnit.MONTH)
+ def first_month(self) -> t.Period:
+ start: None | t.Instant = self.start.offset("first-of", DateUnit.MONTH)
+ if start is None:
+ raise NotImplementedError
return self.__class__((DateUnit.MONTH, start, 1))
@property
- def first_day(self) -> Period:
+ def first_day(self) -> t.Period:
return self.__class__((DateUnit.DAY, self.start, 1))
@property
- def first_week(self) -> Period:
- start: Instant = self.start.offset("first-of", DateUnit.WEEK)
+ def first_week(self) -> t.Period:
+ start: None | t.Instant = self.start.offset("first-of", DateUnit.WEEK)
+ if start is None:
+ raise NotImplementedError
return self.__class__((DateUnit.WEEK, start, 1))
@property
- def first_weekday(self) -> Period:
+ def first_weekday(self) -> t.Period:
return self.__class__((DateUnit.WEEKDAY, self.start, 1))
+
+ @classmethod
+ def eternity(cls) -> t.Period:
+ """Return an eternity period."""
+ return cls((DateUnit.ETERNITY, Instant.eternity(), 0))
diff --git a/openfisca_core/periods/py.typed b/openfisca_core/periods/py.typed
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openfisca_core/periods/tests/helpers/test_helpers.py b/openfisca_core/periods/tests/helpers/test_helpers.py
index bb409323d1..1a351ce835 100644
--- a/openfisca_core/periods/tests/helpers/test_helpers.py
+++ b/openfisca_core/periods/tests/helpers/test_helpers.py
@@ -47,9 +47,19 @@ def test_instant_date_with_an_invalid_argument(arg, error):
[Period((DateUnit.MONTH, Instant((1, 1, 1)), 12)), "200_12"],
[Period((DateUnit.YEAR, Instant((1, 1, 1)), 2)), "300_2"],
[Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"],
- [(DateUnit.DAY, None, 1), "100_1"],
- [(DateUnit.MONTH, None, -1000), "200_-1000"],
],
)
def test_key_period_size(arg, expected):
assert periods.key_period_size(arg) == expected
+
+
+@pytest.mark.parametrize(
+ "arg, error",
+ [
+ [(DateUnit.DAY, None, 1), AttributeError],
+ [(DateUnit.MONTH, None, -1000), AttributeError],
+ ],
+)
+def test_key_period_size_when_an_invalid_argument(arg, error):
+ with pytest.raises(error):
+ periods.key_period_size(arg)
diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py
index 7d50abe102..33aaac2e2b 100644
--- a/openfisca_core/periods/tests/helpers/test_period.py
+++ b/openfisca_core/periods/tests/helpers/test_period.py
@@ -9,11 +9,11 @@
@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)), 0))],
+ ["ETERNITY", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0))],
[
DateUnit.ETERNITY,
- Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf"))),
+ Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0)),
],
[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))],
diff --git a/openfisca_core/periods/types.py b/openfisca_core/periods/types.py
new file mode 100644
index 0000000000..26a6e096d0
--- /dev/null
+++ b/openfisca_core/periods/types.py
@@ -0,0 +1,66 @@
+from __future__ import annotations
+
+from typing import NewType, Protocol
+
+from pendulum.datetime import Date
+
+from openfisca_core import types as t
+
+# New types.
+
+#: For example "2000-01".
+InstantStr = NewType("InstantStr", str)
+
+#: For example "1:2000-01-01:day".
+PeriodStr = NewType("PeriodStr", str)
+
+
+# Periods
+
+
+class DateUnit(t.DateUnit, Protocol):
+ ...
+
+
+class Instant(t.Instant, Protocol):
+ @property
+ def year(self) -> int:
+ ...
+
+ @property
+ def month(self) -> int:
+ ...
+
+ @property
+ def day(self) -> int:
+ ...
+
+ @property
+ def date(self) -> Date:
+ ...
+
+ def __lt__(self, other: object, /) -> bool:
+ ...
+
+ def __le__(self, other: object, /) -> bool:
+ ...
+
+ def offset(self, offset: str | int, unit: DateUnit) -> Instant | None:
+ ...
+
+
+class Period(t.Period, Protocol):
+ @property
+ def size(self) -> int:
+ ...
+
+ @property
+ def start(self) -> Instant:
+ ...
+
+ @property
+ def stop(self) -> Instant:
+ ...
+
+ def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period:
+ ...
diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py
index e3ef6b209a..4cdd7abcff 100644
--- a/openfisca_core/populations/population.py
+++ b/openfisca_core/populations/population.py
@@ -85,7 +85,7 @@ def check_period_validity(
variable_name: str,
period: Optional[Union[int, str, Period]],
) -> None:
- if isinstance(period, (int, str, Period)):
+ if isinstance(period, (int, str, periods.Period)):
return None
stack = traceback.extract_stack()
diff --git a/openfisca_core/projectors/__init__.py b/openfisca_core/projectors/__init__.py
index 28776e3cf9..9582510828 100644
--- a/openfisca_core/projectors/__init__.py
+++ b/openfisca_core/projectors/__init__.py
@@ -21,7 +21,7 @@
#
# See: https://www.python.org/dev/peps/pep-0008/#imports
-from . import typing
+from . import types
from .entity_to_person_projector import EntityToPersonProjector
from .first_person_to_entity_projector import FirstPersonToEntityProjector
from .helpers import get_projector_from_shortcut, projectable
@@ -35,5 +35,5 @@
"projectable",
"Projector",
"UniqueRoleToEntityProjector",
- "typing",
+ "types",
]
diff --git a/openfisca_core/projectors/entity_to_person_projector.py b/openfisca_core/projectors/entity_to_person_projector.py
index ca6245a1f7..392fda08a1 100644
--- a/openfisca_core/projectors/entity_to_person_projector.py
+++ b/openfisca_core/projectors/entity_to_person_projector.py
@@ -4,7 +4,7 @@
class EntityToPersonProjector(Projector):
"""For instance person.family."""
- def __init__(self, entity, parent=None):
+ def __init__(self, entity, parent=None) -> None:
self.reference_entity = entity
self.parent = parent
diff --git a/openfisca_core/projectors/first_person_to_entity_projector.py b/openfisca_core/projectors/first_person_to_entity_projector.py
index 4b4e7b7994..d986460cdc 100644
--- a/openfisca_core/projectors/first_person_to_entity_projector.py
+++ b/openfisca_core/projectors/first_person_to_entity_projector.py
@@ -4,7 +4,7 @@
class FirstPersonToEntityProjector(Projector):
"""For instance famille.first_person."""
- def __init__(self, entity, parent=None):
+ def __init__(self, entity, parent=None) -> None:
self.target_entity = entity
self.reference_entity = entity.members
self.parent = parent
diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py
index b3b7e6f2d3..d3d8a21c7b 100644
--- a/openfisca_core/projectors/helpers.py
+++ b/openfisca_core/projectors/helpers.py
@@ -1,12 +1,10 @@
from __future__ import annotations
-from collections.abc import Mapping
-
-from openfisca_core.types import GroupEntity, Role, SingleEntity
+from collections.abc import Iterable, Mapping
from openfisca_core import entities, projectors
-from .typing import GroupPopulation, Population
+from . import types as t
def projectable(function):
@@ -19,10 +17,10 @@ def projectable(function):
def get_projector_from_shortcut(
- population: Population | GroupPopulation,
+ population: t.CorePopulation,
shortcut: str,
- parent: projectors.Projector | None = None,
-) -> projectors.Projector | None:
+ parent: t.Projector | None = None,
+) -> t.Projector | None:
"""Get a projector from a shortcut.
Projectors are used to project an invidividual Population's or a
@@ -46,7 +44,7 @@ def get_projector_from_shortcut(
of a specific Simulation and TaxBenefitSystem.
Args:
- population (Population | GroupPopulation): Where to project from.
+ population (SinglePopulation | GroupPopulation): Where to project from.
shortcut (str): Where to project to.
parent: ???
@@ -110,12 +108,10 @@ def get_projector_from_shortcut(
"""
- entity: SingleEntity | GroupEntity = population.entity
+ entity: t.CoreEntity = population.entity
if isinstance(entity, entities.Entity):
- populations: Mapping[
- str, Population | GroupPopulation
- ] = population.simulation.populations
+ populations: Mapping[str, t.CorePopulation] = population.simulation.populations
if shortcut not in populations.keys():
return None
@@ -126,7 +122,8 @@ def get_projector_from_shortcut(
return projectors.FirstPersonToEntityProjector(population, parent)
if isinstance(entity, entities.GroupEntity):
- role: Role | None = entities.find_role(entity.roles, shortcut, total=1)
+ roles: Iterable[t.Role] = entity.roles
+ role: t.Role | None = entities.find_role(roles, shortcut, total=1)
if role is not None:
return projectors.UniqueRoleToEntityProjector(population, role, parent)
diff --git a/openfisca_core/projectors/types.py b/openfisca_core/projectors/types.py
new file mode 100644
index 0000000000..e145cc9a15
--- /dev/null
+++ b/openfisca_core/projectors/types.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Protocol
+
+from openfisca_core import types as t
+
+# Entities
+
+
+class CoreEntity(t.CoreEntity, Protocol):
+ ...
+
+
+class Role(t.Role, Protocol):
+ ...
+
+
+# Projectors
+
+
+class Projector(Protocol):
+ ...
+
+
+# Populations
+
+
+class CorePopulation(t.CorePopulation, Protocol):
+ @property
+ def entity(self) -> CoreEntity:
+ ...
+
+ @property
+ def simulation(self) -> Simulation:
+ ...
+
+
+# Simulations
+
+
+class Simulation(t.Simulation, Protocol):
+ @property
+ def populations(self) -> Mapping[str, CorePopulation]:
+ ...
diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py
deleted file mode 100644
index 186f90e30c..0000000000
--- a/openfisca_core/projectors/typing.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from __future__ import annotations
-
-from collections.abc import Mapping
-from typing import Protocol
-
-from openfisca_core.types import GroupEntity, SingleEntity
-
-
-class Population(Protocol):
- @property
- def entity(self) -> SingleEntity:
- ...
-
- @property
- def simulation(self) -> Simulation:
- ...
-
-
-class GroupPopulation(Protocol):
- @property
- def entity(self) -> GroupEntity:
- ...
-
- @property
- def simulation(self) -> Simulation:
- ...
-
-
-class Simulation(Protocol):
- @property
- def populations(self) -> Mapping[str, Population | GroupPopulation]:
- ...
diff --git a/openfisca_core/projectors/unique_role_to_entity_projector.py b/openfisca_core/projectors/unique_role_to_entity_projector.py
index fed2f249ca..c565484339 100644
--- a/openfisca_core/projectors/unique_role_to_entity_projector.py
+++ b/openfisca_core/projectors/unique_role_to_entity_projector.py
@@ -4,7 +4,7 @@
class UniqueRoleToEntityProjector(Projector):
"""For instance famille.declarant_principal."""
- def __init__(self, entity, role, parent=None):
+ def __init__(self, entity, role, parent=None) -> None:
self.target_entity = entity
self.reference_entity = entity.members
self.parent = parent
diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py
index 93becda960..c4525525d0 100644
--- a/openfisca_core/simulations/simulation.py
+++ b/openfisca_core/simulations/simulation.py
@@ -2,7 +2,7 @@
from typing import Dict, Mapping, NamedTuple, Optional, Set
-from openfisca_core.types import Population, TaxBenefitSystem, Variable
+from openfisca_core.types import SinglePopulation, TaxBenefitSystem, Variable
import tempfile
import warnings
@@ -19,13 +19,13 @@ class Simulation:
"""
tax_benefit_system: TaxBenefitSystem
- populations: Dict[str, Population]
+ populations: Dict[str, SinglePopulation]
invalidated_caches: Set[Cache]
def __init__(
self,
tax_benefit_system: TaxBenefitSystem,
- populations: Mapping[str, Population],
+ populations: Mapping[str, SinglePopulation],
):
"""
This constructor is reserved for internal use; see :any:`SimulationBuilder`,
@@ -531,7 +531,7 @@ def set_input(self, variable_name: str, period, value):
return
self.get_holder(variable_name).set_input(period, value)
- def get_variable_population(self, variable_name: str) -> Population:
+ def get_variable_population(self, variable_name: str) -> SinglePopulation:
variable: Optional[Variable]
variable = self.tax_benefit_system.get_variable(
@@ -543,7 +543,9 @@ def get_variable_population(self, variable_name: str) -> Population:
return self.populations[variable.entity.key]
- def get_population(self, plural: Optional[str] = None) -> Optional[Population]:
+ def get_population(
+ self, plural: Optional[str] = None
+ ) -> Optional[SinglePopulation]:
return next(
(
population
@@ -556,7 +558,7 @@ def get_population(self, plural: Optional[str] = None) -> Optional[Population]:
def get_entity(
self,
plural: Optional[str] = None,
- ) -> Optional[Population]:
+ ) -> Optional[SinglePopulation]:
population = self.get_population(plural)
return population and population.entity
diff --git a/openfisca_core/types.py b/openfisca_core/types.py
index b34a555434..fcb7f4898d 100644
--- a/openfisca_core/types.py
+++ b/openfisca_core/types.py
@@ -1,20 +1,48 @@
from __future__ import annotations
-import typing_extensions
-from collections.abc import Sequence
+from collections.abc import Iterable, Sequence, Sized
from numpy.typing import NDArray
-from typing import Any, TypeVar
+from typing import Any, TypeVar, Union
from typing_extensions import Protocol, TypeAlias
-import abc
-
import numpy
+# Type aliases
+
+#: Type for arrays of any type.
+ArrayAny: TypeAlias = numpy.generic
+
+#: Type for arrays of booleans.
+ArrayBool: TypeAlias = numpy.bool_
+
+#: Type for arrays of bytes.
+ArrayBytes: TypeAlias = numpy.bytes_
+
+#: Type for arrays of dates.
+ArrayDate: TypeAlias = numpy.datetime64
+
+#: Type for arrays of enums.
+ArrayEnum: TypeAlias = numpy.int16
+
+#: Type for arrays of floats.
+ArrayFloat: TypeAlias = numpy.float32
+
+#: Type for arrays of integers.
+ArrayInt: TypeAlias = numpy.int32
+
+#: Type for arrays of Python objects.
+ArrayObject: TypeAlias = numpy.object_
+
+#: Type for arrays of strings.
+ArrayStr: TypeAlias = numpy.str_
+
+#: Generic numpy type an array can have.
N = TypeVar("N", bound=numpy.generic, covariant=True)
#: Type representing an numpy array.
Array: TypeAlias = NDArray[N]
+#: Generic type a sequence can have.
L = TypeVar("L")
#: Type representing an array-like object.
@@ -26,6 +54,12 @@
#: Type variable representing a value.
A = TypeVar("A", covariant=True)
+#: Generic covariant type.
+T_cov = TypeVar("T_cov", covariant=True)
+
+#: Generic contravariant type.
+T_con = TypeVar("T_con", contravariant=True)
+
# Entities
@@ -34,15 +68,12 @@ class CoreEntity(Protocol):
key: Any
plural: Any
- @abc.abstractmethod
def check_role_validity(self, role: Any) -> None:
...
- @abc.abstractmethod
def check_variable_defined_for_entity(self, variable_name: Any) -> None:
...
- @abc.abstractmethod
def get_variable(
self,
variable_name: Any,
@@ -73,11 +104,9 @@ def key(self) -> str:
class Holder(Protocol):
- @abc.abstractmethod
def clone(self, population: Any) -> Holder:
...
- @abc.abstractmethod
def get_memory_usage(self) -> Any:
...
@@ -85,7 +114,6 @@ def get_memory_usage(self) -> Any:
# Parameters
-@typing_extensions.runtime_checkable
class ParameterNodeAtInstant(Protocol):
...
@@ -93,51 +121,57 @@ class ParameterNodeAtInstant(Protocol):
# Periods
-class Instant(Protocol):
- ...
+class Indexable(Protocol[T_cov]):
+ def __getitem__(self, index: int, /) -> T_cov:
+ ...
-@typing_extensions.runtime_checkable
-class Period(Protocol):
- @property
- @abc.abstractmethod
- def start(self) -> Any:
+class DateUnit(Protocol):
+ def __contains__(self, other: object, /) -> bool:
...
+
+class Instant(Indexable[int], Iterable[int], Sized, Protocol):
+ ...
+
+
+class Period(Indexable[Union[DateUnit, Instant, int]], Protocol):
@property
- @abc.abstractmethod
- def unit(self) -> Any:
+ def unit(self) -> DateUnit:
...
# Populations
-class Population(Protocol):
+class CorePopulation(Protocol):
+ ...
+
+
+class SinglePopulation(CorePopulation, Protocol):
entity: Any
- @abc.abstractmethod
def get_holder(self, variable_name: Any) -> Any:
...
+class GroupPopulation(CorePopulation, Protocol):
+ ...
+
+
# Simulations
class Simulation(Protocol):
- @abc.abstractmethod
def calculate(self, variable_name: Any, period: Any) -> Any:
...
- @abc.abstractmethod
def calculate_add(self, variable_name: Any, period: Any) -> Any:
...
- @abc.abstractmethod
def calculate_divide(self, variable_name: Any, period: Any) -> Any:
...
- @abc.abstractmethod
def get_population(self, plural: Any | None) -> Any:
...
@@ -148,7 +182,6 @@ def get_population(self, plural: Any | None) -> Any:
class TaxBenefitSystem(Protocol):
person_entity: Any
- @abc.abstractmethod
def get_variable(
self,
variable_name: Any,
@@ -165,10 +198,9 @@ class Variable(Protocol):
class Formula(Protocol):
- @abc.abstractmethod
def __call__(
self,
- population: Population,
+ population: GroupPopulation,
instant: Instant,
params: Params,
) -> Array[Any]:
@@ -176,6 +208,5 @@ def __call__(
class Params(Protocol):
- @abc.abstractmethod
def __call__(self, instant: Instant) -> ParameterNodeAtInstant:
...
diff --git a/openfisca_core/variables/config.py b/openfisca_core/variables/config.py
index 54270145bf..90faa7d127 100644
--- a/openfisca_core/variables/config.py
+++ b/openfisca_core/variables/config.py
@@ -1,27 +1,25 @@
import datetime
-import numpy
-
-from openfisca_core import indexed_enums
-from openfisca_core.indexed_enums import Enum
+from openfisca_core import indexed_enums as enums
+from openfisca_core import types as t
VALUE_TYPES = {
bool: {
- "dtype": numpy.bool_,
+ "dtype": t.ArrayBool,
"default": False,
"json_type": "boolean",
"formatted_value_type": "Boolean",
"is_period_size_independent": True,
},
int: {
- "dtype": numpy.int32,
+ "dtype": t.ArrayInt,
"default": 0,
"json_type": "integer",
"formatted_value_type": "Int",
"is_period_size_independent": False,
},
float: {
- "dtype": numpy.float32,
+ "dtype": t.ArrayFloat,
"default": 0,
"json_type": "number",
"formatted_value_type": "Float",
@@ -34,8 +32,8 @@
"formatted_value_type": "String",
"is_period_size_independent": True,
},
- Enum: {
- "dtype": indexed_enums.ENUM_ARRAY_DTYPE,
+ enums.Enum: {
+ "dtype": t.ArrayEnum,
"json_type": "string",
"formatted_value_type": "String",
"is_period_size_independent": True,
diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py
index e70c0d05d9..1b073a1bdc 100644
--- a/openfisca_core/variables/variable.py
+++ b/openfisca_core/variables/variable.py
@@ -2,8 +2,6 @@
from typing import Optional, Union
-from openfisca_core.types import Formula, Instant
-
import datetime
import re
import textwrap
@@ -12,6 +10,7 @@
import sortedcontainers
from openfisca_core import periods, tools
+from openfisca_core import types as t
from openfisca_core.entities import Entity, GroupEntity
from openfisca_core.indexed_enums import Enum, EnumArray
from openfisca_core.periods import DateUnit, Period
@@ -374,8 +373,8 @@ def get_introspection_data(cls):
def get_formula(
self,
- period: Union[Instant, Period, str, int] = None,
- ) -> Optional[Formula]:
+ period: Union[t.Instant, t.Period, str, int] = None,
+ ) -> Optional[t.Formula]:
"""Returns the formula to compute the variable at the given period.
If no period is given and the variable has several formulas, the method
@@ -389,7 +388,7 @@ def get_formula(
"""
- instant: Optional[Instant]
+ instant: Optional[t.Instant]
if not self.formulas:
return None
diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk
index 445abba10b..4d1f3e0977 100644
--- a/openfisca_tasks/lint.mk
+++ b/openfisca_tasks/lint.mk
@@ -42,6 +42,9 @@ check-types:
@mypy \
openfisca_core/commons \
openfisca_core/entities \
+ openfisca_core/indexed_enums \
+ openfisca_core/periods \
+ openfisca_core/projectors \
openfisca_core/types.py
@$(call print_pass,$@:)
diff --git a/setup.py b/setup.py
index cca107bee8..9aa018fca8 100644
--- a/setup.py
+++ b/setup.py
@@ -31,7 +31,7 @@
"dpath >=2.1.4, <3.0",
"numexpr >=2.8.4, <3.0",
"numpy >=1.24.2, <1.25",
- "pendulum >=2.1.2, <3.0.0",
+ "pendulum >=3.0.0, <4.0.0",
"psutil >=5.9.4, <6.0",
"pytest >=7.2.2, <8.0",
"sortedcontainers >=2.4.0, <3.0",
@@ -67,7 +67,7 @@
setup(
name="OpenFisca-Core",
- version="41.5.5",
+ version="41.5.8",
author="OpenFisca Team",
author_email="contact@openfisca.org",
classifiers=[