From a183a3a55f9fc528d8c443c6cf7c277e247d1020 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 13:37:31 +0200 Subject: [PATCH 01/10] chore(deps): update pendulum --- openfisca_core/periods/period_.py | 8 ++++---- openfisca_tasks/lint.mk | 1 + setup.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 0dcf960bb..14726d536 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -348,14 +348,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 = pendulum.interval(start, 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 = pendulum.interval(start, cease) + return delta.in_weeks() if self.unit == DateUnit.WEEK: return self.size diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 445abba10..7f90aa5d5 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -42,6 +42,7 @@ check-types: @mypy \ openfisca_core/commons \ openfisca_core/entities \ + openfisca_core/periods \ openfisca_core/types.py @$(call print_pass,$@:) diff --git a/setup.py b/setup.py index 2f1c7089b..6b573a28b 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 >=8.3.3, <9.0", "sortedcontainers >=2.4.0, <3.0", From 99ba51a6552eaf870c7df1dd54f6c2cdf03c3ae9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 18 Sep 2024 00:30:21 +0200 Subject: [PATCH 02/10] refactor(types): do not use abstract classes --- openfisca_core/types.py | 55 +++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/openfisca_core/types.py b/openfisca_core/types.py index 16a1f0e90..5134c518d 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -1,13 +1,10 @@ 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 N = TypeVar("N", bound=numpy.generic, covariant=True) @@ -26,6 +23,10 @@ #: Type variable representing a value. A = TypeVar("A", covariant=True) +#: Generic type vars. +T_cov = TypeVar("T_cov", covariant=True) +T_con = TypeVar("T_con", contravariant=True) + # Entities @@ -34,13 +35,8 @@ 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, @@ -67,35 +63,36 @@ def key(self) -> str: ... class Holder(Protocol): - @abc.abstractmethod def clone(self, population: Any) -> Holder: ... - - @abc.abstractmethod def get_memory_usage(self) -> Any: ... # Parameters -@typing_extensions.runtime_checkable class ParameterNodeAtInstant(Protocol): ... # Periods -class Instant(Protocol): ... +class Container(Protocol[T_con]): + def __contains__(self, item: T_con, /) -> bool: ... -@typing_extensions.runtime_checkable -class Period(Protocol): - @property - @abc.abstractmethod - def start(self) -> Any: ... +class Indexable(Protocol[T_cov]): + def __getitem__(self, index: int, /) -> T_cov: ... + + +class DateUnit(Container[str], Protocol): ... + +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 @@ -104,7 +101,6 @@ def unit(self) -> Any: ... class Population(Protocol): entity: Any - @abc.abstractmethod def get_holder(self, variable_name: Any) -> Any: ... @@ -112,16 +108,9 @@ def get_holder(self, variable_name: Any) -> Any: ... 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: ... @@ -131,13 +120,11 @@ def get_population(self, plural: Any | None) -> Any: ... class TaxBenefitSystem(Protocol): person_entity: Any - @abc.abstractmethod def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Any | None: - """Abstract method.""" + ) -> Any | None: ... # Variables @@ -148,7 +135,6 @@ class Variable(Protocol): class Formula(Protocol): - @abc.abstractmethod def __call__( self, population: Population, @@ -158,5 +144,4 @@ def __call__( class Params(Protocol): - @abc.abstractmethod def __call__(self, instant: Instant) -> ParameterNodeAtInstant: ... From 9a43147aa1a9cc1ee68db8092c0c7177dd770fec Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 11:43:34 +0200 Subject: [PATCH 03/10] refactor(commons): reuse array type defs --- openfisca_core/commons/__init__.py | 24 ++++++++++++---------- openfisca_core/commons/dummy.py | 3 +++ openfisca_core/commons/formulas.py | 25 +++++++++++++--------- openfisca_core/commons/misc.py | 9 ++++++-- openfisca_core/commons/rates.py | 33 ++++++++++++++++-------------- openfisca_core/commons/types.py | 3 +++ 6 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 openfisca_core/commons/types.py diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index 807abec77..37204d859 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -50,18 +50,20 @@ """ -# Official Public API - +from . import types +from .dummy import Dummy from .formulas import apply_thresholds, concat, switch from .misc import empty_clone, stringify_array from .rates import average_rate, marginal_rate -__all__ = ["apply_thresholds", "concat", "switch"] -__all__ = ["empty_clone", "stringify_array", *__all__] -__all__ = ["average_rate", "marginal_rate", *__all__] - -# Deprecated - -from .dummy import Dummy - -__all__ = ["Dummy", *__all__] +__all__ = [ + "Dummy", + "apply_thresholds", + "average_rate", + "concat", + "empty_clone", + "marginal_rate", + "stringify_array", + "switch", + "types", +] diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 5135a8f55..b9fc31d89 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -20,3 +20,6 @@ def __init__(self) -> None: "and will be removed in the future.", ] warnings.warn(" ".join(message), DeprecationWarning, stacklevel=2) + + +__all__ = ["Dummy"] diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 909c4cd14..eb49a9f52 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,16 +1,17 @@ +from __future__ import annotations + from collections.abc import Mapping -from typing import Union import numpy -from openfisca_core import types as t +from . import types as t def apply_thresholds( - input: t.Array[numpy.float64], + input: t.Array[numpy.float32], thresholds: t.ArrayLike[float], choices: t.ArrayLike[float], -) -> t.Array[numpy.float64]: +) -> t.Array[numpy.float32]: """Makes a choice based on an input and thresholds. From a list of ``choices``, this function selects one of these values @@ -38,7 +39,8 @@ def apply_thresholds( array([10, 10, 15, 15, 20]) """ - condlist: list[Union[t.Array[numpy.bool_], bool]] + + condlist: list[t.Array[numpy.bool_] | bool] condlist = [input <= threshold for threshold in thresholds] if len(condlist) == len(choices) - 1: @@ -54,8 +56,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: t.Array[numpy.str_] | t.ArrayLike[str], + that: t.Array[numpy.str_] | t.ArrayLike[str], ) -> t.Array[numpy.str_]: """Concatenates the values of two arrays. @@ -84,10 +86,10 @@ def concat( def switch( - conditions: t.Array[numpy.float64], + conditions: t.Array[numpy.float32], value_by_condition: Mapping[float, float], -) -> t.Array[numpy.float64]: - """Mimicks a switch statement. +) -> t.Array[numpy.float32]: + """Mimick a switch statement. Given an array of conditions, returns an array of the same size, replacing each condition item with the matching given value. @@ -117,3 +119,6 @@ def switch( condlist = [conditions == condition for condition in value_by_condition] return numpy.select(condlist, tuple(value_by_condition.values())) + + +__all__ = ["apply_thresholds", "concat", "switch"] diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 3c9cd5fea..7e4fd5093 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,4 +1,6 @@ -from typing import Optional, TypeVar +from __future__ import annotations + +from typing import TypeVar import numpy @@ -44,7 +46,7 @@ def empty_clone(original: T) -> T: return new -def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str: +def stringify_array(array: None | t.Array[numpy.generic]) -> str: """Generates a clean string representation of a numpy array. Args: @@ -76,3 +78,6 @@ def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str: return "None" return f"[{', '.join(str(cell) for cell in array)}]" + + +__all__ = ["empty_clone", "stringify_array"] diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index e5c3114a7..3a1ec661c 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -1,16 +1,16 @@ -from typing import Optional - -from openfisca_core.types import Array, ArrayLike +from __future__ import annotations import numpy +from . import types as t + def average_rate( - target: Array[numpy.float64], - varying: ArrayLike[float], - trim: Optional[ArrayLike[float]] = None, -) -> Array[numpy.float64]: - """Computes the average rate of a target net income. + target: t.Array[numpy.float32], + varying: t.ArrayLike[float], + trim: None | t.ArrayLike[float] = None, +) -> t.Array[numpy.float32]: + """Compute the average rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross income. Optionally, a ``trim`` can be applied consisting of the lower and @@ -40,8 +40,8 @@ def average_rate( array([ nan, 0. , -0.5]) """ - average_rate: Array[numpy.float64] + average_rate: t.Array[numpy.float32] average_rate = 1 - target / varying if trim is not None: @@ -61,11 +61,11 @@ def average_rate( def marginal_rate( - target: Array[numpy.float64], - varying: Array[numpy.float64], - trim: Optional[ArrayLike[float]] = None, -) -> Array[numpy.float64]: - """Computes the marginal rate of a target net income. + target: t.Array[numpy.float32], + varying: t.Array[numpy.float32], + trim: None | t.ArrayLike[float] = None, +) -> t.Array[numpy.float32]: + """Compute the marginal rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross income. Optionally, a ``trim`` can be applied consisting of the lower and @@ -95,8 +95,8 @@ def marginal_rate( array([nan, 0.5]) """ - marginal_rate: Array[numpy.float64] + marginal_rate: t.Array[numpy.float32] marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) if trim is not None: @@ -113,3 +113,6 @@ def marginal_rate( ) return marginal_rate + + +__all__ = ["average_rate", "marginal_rate"] diff --git a/openfisca_core/commons/types.py b/openfisca_core/commons/types.py new file mode 100644 index 000000000..39c067f45 --- /dev/null +++ b/openfisca_core/commons/types.py @@ -0,0 +1,3 @@ +from openfisca_core.types import Array, ArrayLike + +__all__ = ["Array", "ArrayLike"] From 66339b83c97227b5654f1417ce1f4c88653bb1ea Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 18:23:56 +0200 Subject: [PATCH 04/10] refactor(tools): move eval_expression to commons --- openfisca_core/commons/__init__.py | 4 ++- openfisca_core/commons/misc.py | 29 ++++++++++++++++++- openfisca_core/indexed_enums/__init__.py | 14 +++++++-- openfisca_core/indexed_enums/enum.py | 2 +- openfisca_core/indexed_enums/enum_array.py | 6 ++-- openfisca_core/indexed_enums/types.py | 3 ++ openfisca_core/parameters/parameter_node.py | 6 ++-- openfisca_core/periods/period_.py | 8 ++--- openfisca_core/populations/population.py | 2 +- .../simulations/simulation_builder.py | 4 +-- .../taxscales/abstract_rate_tax_scale.py | 2 +- .../taxscales/abstract_tax_scale.py | 4 +-- .../linear_average_rate_tax_scale.py | 4 +-- .../taxscales/marginal_amount_tax_scale.py | 4 +-- .../taxscales/marginal_rate_tax_scale.py | 12 ++++---- .../taxscales/rate_tax_scale_like.py | 4 +-- .../taxscales/single_amount_tax_scale.py | 4 +-- openfisca_core/taxscales/tax_scale_like.py | 4 +-- openfisca_core/tools/__init__.py | 12 ++------ openfisca_core/tools/simulation_dumper.py | 4 +-- openfisca_core/variables/variable.py | 4 +-- setup.cfg | 2 +- stubs/numexpr/__init__.pyi | 9 ++++++ 23 files changed, 94 insertions(+), 53 deletions(-) create mode 100644 openfisca_core/indexed_enums/types.py create mode 100644 stubs/numexpr/__init__.pyi diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index 37204d859..039bd0673 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -8,6 +8,7 @@ * :func:`.average_rate` * :func:`.concat` * :func:`.empty_clone` + * :func:`.eval_expression` * :func:`.marginal_rate` * :func:`.stringify_array` * :func:`.switch` @@ -53,7 +54,7 @@ from . import types from .dummy import Dummy from .formulas import apply_thresholds, concat, switch -from .misc import empty_clone, stringify_array +from .misc import empty_clone, eval_expression, stringify_array from .rates import average_rate, marginal_rate __all__ = [ @@ -62,6 +63,7 @@ "average_rate", "concat", "empty_clone", + "eval_expression", "marginal_rate", "stringify_array", "switch", diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 7e4fd5093..99b830099 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -2,6 +2,7 @@ from typing import TypeVar +import numexpr import numpy from openfisca_core import types as t @@ -80,4 +81,30 @@ def stringify_array(array: None | t.Array[numpy.generic]) -> str: return f"[{', '.join(str(cell) for cell in array)}]" -__all__ = ["empty_clone", "stringify_array"] +def eval_expression( + expression: str, +) -> str | t.Array[numpy.bool_] | t.Array[numpy.int32] | t.Array[numpy.float32]: + """Evaluate a string expression to a numpy array. + + Args: + expression(str): An expression to evaluate. + + Returns: + :obj:`object`: The result of the evaluation. + + Examples: + >>> eval_expression("1 + 2") + array(3, dtype=int32) + + >>> eval_expression("salary") + 'salary' + + """ + + try: + return numexpr.evaluate(expression) + except (KeyError, TypeError): + return expression + + +__all__ = ["empty_clone", "eval_expression", "stringify_array"] diff --git a/openfisca_core/indexed_enums/__init__.py b/openfisca_core/indexed_enums/__init__.py index 874a7e1f9..9c4ff7dd6 100644 --- a/openfisca_core/indexed_enums/__init__.py +++ b/openfisca_core/indexed_enums/__init__.py @@ -21,6 +21,14 @@ # # 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 . import types +from .config import ENUM_ARRAY_DTYPE +from .enum import Enum +from .enum_array import EnumArray + +__all__ = [ + "ENUM_ARRAY_DTYPE", + "Enum", + "EnumArray", + "types", +] diff --git a/openfisca_core/indexed_enums/enum.py b/openfisca_core/indexed_enums/enum.py index 25b02cee7..ec1afa45a 100644 --- a/openfisca_core/indexed_enums/enum.py +++ b/openfisca_core/indexed_enums/enum.py @@ -30,7 +30,7 @@ def __init__(self, name: str) -> None: @classmethod def encode( cls, - array: EnumArray | numpy.int_ | numpy.float64 | numpy.object_, + array: EnumArray | numpy.int32 | numpy.float32 | numpy.object_, ) -> 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 diff --git a/openfisca_core/indexed_enums/enum_array.py b/openfisca_core/indexed_enums/enum_array.py index 86b55f9f4..1b6c512b8 100644 --- a/openfisca_core/indexed_enums/enum_array.py +++ b/openfisca_core/indexed_enums/enum_array.py @@ -5,6 +5,8 @@ import numpy +from . import types as t + if typing.TYPE_CHECKING: from openfisca_core.indexed_enums import Enum @@ -20,7 +22,7 @@ class EnumArray(numpy.ndarray): # https://docs.scipy.org/doc/numpy-1.13.0/user/basics.subclassing.html#slightly-more-realistic-example-attribute-added-to-existing-array. def __new__( cls, - input_array: numpy.int_, + input_array: t.Array[numpy.int16], possible_values: type[Enum] | None = None, ) -> EnumArray: obj = numpy.asarray(input_array).view(cls) @@ -28,7 +30,7 @@ def __new__( return obj # See previous comment - def __array_finalize__(self, obj: numpy.int_ | None) -> None: + def __array_finalize__(self, obj: numpy.int32 | None) -> None: if obj is None: return diff --git a/openfisca_core/indexed_enums/types.py b/openfisca_core/indexed_enums/types.py new file mode 100644 index 000000000..43c38780f --- /dev/null +++ b/openfisca_core/indexed_enums/types.py @@ -0,0 +1,3 @@ +from openfisca_core.types import Array + +__all__ = ["Array"] diff --git a/openfisca_core/parameters/parameter_node.py b/openfisca_core/parameters/parameter_node.py index 2be3a9acf..6f43379b3 100644 --- a/openfisca_core/parameters/parameter_node.py +++ b/openfisca_core/parameters/parameter_node.py @@ -1,6 +1,6 @@ from __future__ import annotations -import typing +from collections.abc import Iterable import copy import os @@ -16,9 +16,7 @@ class ParameterNode(AtInstantLike): """A node in the legislation `parameter tree `_.""" - _allowed_keys: None | (typing.Iterable[str]) = ( - None # By default, no restriction on the keys - ) + _allowed_keys: None | Iterable[str] = None # By default, no restriction on the keys def __init__(self, name="", directory_path=None, data=None, file_path=None) -> None: """Instantiate a ParameterNode either from a dict, (using `data`), or from a directory containing YAML files (using `directory_path`). diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 14726d536..0dcf960bb 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -348,14 +348,14 @@ def size_in_weeks(self): if self.unit == DateUnit.YEAR: start = self.start.date cease = start.add(years=self.size) - delta = pendulum.interval(start, cease) - return delta.in_weeks() + delta = pendulum.period(start, cease) + return delta.as_interval().weeks if self.unit == DateUnit.MONTH: start = self.start.date cease = start.add(months=self.size) - delta = pendulum.interval(start, cease) - return delta.in_weeks() + delta = pendulum.period(start, cease) + return delta.as_interval().weeks if self.unit == DateUnit.WEEK: return self.size diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index f9eee1c2a..06acc05d2 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -84,7 +84,7 @@ def check_period_validity( variable_name: str, period: int | str | Period | None, ) -> None: - if isinstance(period, (int, str, Period)): + if isinstance(period, (int, str, periods.Period)): return stack = traceback.extract_stack() diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 1ebb49923..96d451cd7 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -369,7 +369,7 @@ def join_with_persons( if numpy.issubdtype(roles_array.dtype, numpy.integer): group_population.members_role = numpy.array(flattened_roles)[roles_array] elif len(flattened_roles) == 0: - group_population.members_role = numpy.int64(0) + group_population.members_role = numpy.int16(0) else: group_population.members_role = numpy.select( [roles_array == role.key for role in flattened_roles], @@ -754,7 +754,7 @@ def expand_axes(self) -> None: ) # Adjust ids original_ids: list[str] = self.get_ids(entity_name) * cell_count - indices: Array[numpy.int_] = numpy.arange( + indices: Array[numpy.int16] = numpy.arange( 0, cell_count * self.entity_counts[entity_name], ) diff --git a/openfisca_core/taxscales/abstract_rate_tax_scale.py b/openfisca_core/taxscales/abstract_rate_tax_scale.py index 84ab4eb91..9d828ed67 100644 --- a/openfisca_core/taxscales/abstract_rate_tax_scale.py +++ b/openfisca_core/taxscales/abstract_rate_tax_scale.py @@ -9,7 +9,7 @@ if typing.TYPE_CHECKING: import numpy - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class AbstractRateTaxScale(RateTaxScaleLike): diff --git a/openfisca_core/taxscales/abstract_tax_scale.py b/openfisca_core/taxscales/abstract_tax_scale.py index 933f36d47..de9a6348c 100644 --- a/openfisca_core/taxscales/abstract_tax_scale.py +++ b/openfisca_core/taxscales/abstract_tax_scale.py @@ -9,7 +9,7 @@ if typing.TYPE_CHECKING: import numpy - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class AbstractTaxScale(TaxScaleLike): @@ -21,7 +21,7 @@ def __init__( self, name: str | None = None, option: typing.Any = None, - unit: numpy.int_ = None, + unit: numpy.int16 = None, ) -> None: message = [ "The 'AbstractTaxScale' class has been deprecated since", diff --git a/openfisca_core/taxscales/linear_average_rate_tax_scale.py b/openfisca_core/taxscales/linear_average_rate_tax_scale.py index ec1b22e0c..ffccfc220 100644 --- a/openfisca_core/taxscales/linear_average_rate_tax_scale.py +++ b/openfisca_core/taxscales/linear_average_rate_tax_scale.py @@ -13,7 +13,7 @@ log = logging.getLogger(__name__) if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class LinearAverageRateTaxScale(RateTaxScaleLike): @@ -21,7 +21,7 @@ def calc( self, tax_base: NumericalArray, right: bool = False, - ) -> numpy.float64: + ) -> numpy.float32: if len(self.rates) == 1: return tax_base * self.rates[0] diff --git a/openfisca_core/taxscales/marginal_amount_tax_scale.py b/openfisca_core/taxscales/marginal_amount_tax_scale.py index ac021351b..aa96bff57 100644 --- a/openfisca_core/taxscales/marginal_amount_tax_scale.py +++ b/openfisca_core/taxscales/marginal_amount_tax_scale.py @@ -7,7 +7,7 @@ from .amount_tax_scale_like import AmountTaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class MarginalAmountTaxScale(AmountTaxScaleLike): @@ -15,7 +15,7 @@ def calc( self, tax_base: NumericalArray, right: bool = False, - ) -> numpy.float64: + ) -> numpy.float32: """Matches the input amount to a set of brackets and returns the sum of cell values from the lowest bracket to the one containing the input. """ diff --git a/openfisca_core/taxscales/marginal_rate_tax_scale.py b/openfisca_core/taxscales/marginal_rate_tax_scale.py index c81da8e7e..803a5f854 100644 --- a/openfisca_core/taxscales/marginal_rate_tax_scale.py +++ b/openfisca_core/taxscales/marginal_rate_tax_scale.py @@ -12,7 +12,7 @@ from .rate_tax_scale_like import RateTaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class MarginalRateTaxScale(RateTaxScaleLike): @@ -37,7 +37,7 @@ def calc( tax_base: NumericalArray, factor: float = 1.0, round_base_decimals: int | None = None, - ) -> numpy.float64: + ) -> numpy.float32: """Compute the tax amount for the given tax bases by applying a taxscale. :param ndarray tax_base: Array of the tax bases. @@ -119,7 +119,7 @@ def marginal_rates( tax_base: NumericalArray, factor: float = 1.0, round_base_decimals: int | None = None, - ) -> numpy.float64: + ) -> numpy.float32: """Compute the marginal tax rates relevant for the given tax bases. :param ndarray tax_base: Array of the tax bases. @@ -149,8 +149,8 @@ def marginal_rates( def rate_from_bracket_indice( self, - bracket_indice: numpy.int_, - ) -> numpy.float64: + bracket_indice: numpy.int16, + ) -> numpy.float32: """Compute the relevant tax rates for the given bracket indices. :param: ndarray bracket_indice: Array of the bracket indices. @@ -186,7 +186,7 @@ def rate_from_bracket_indice( def rate_from_tax_base( self, tax_base: NumericalArray, - ) -> numpy.float64: + ) -> numpy.float32: """Compute the relevant tax rates for the given tax bases. :param: ndarray tax_base: Array of the tax bases. diff --git a/openfisca_core/taxscales/rate_tax_scale_like.py b/openfisca_core/taxscales/rate_tax_scale_like.py index 60ea9c20e..288226f11 100644 --- a/openfisca_core/taxscales/rate_tax_scale_like.py +++ b/openfisca_core/taxscales/rate_tax_scale_like.py @@ -14,7 +14,7 @@ from .tax_scale_like import TaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class RateTaxScaleLike(TaxScaleLike, abc.ABC): @@ -128,7 +128,7 @@ def bracket_indices( tax_base: NumericalArray, factor: float = 1.0, round_decimals: int | None = None, - ) -> numpy.int_: + ) -> numpy.int32: """Compute the relevant bracket indices for the given tax bases. :param ndarray tax_base: Array of the tax bases. diff --git a/openfisca_core/taxscales/single_amount_tax_scale.py b/openfisca_core/taxscales/single_amount_tax_scale.py index 1a3939639..1c8cf69a3 100644 --- a/openfisca_core/taxscales/single_amount_tax_scale.py +++ b/openfisca_core/taxscales/single_amount_tax_scale.py @@ -7,7 +7,7 @@ from openfisca_core.taxscales import AmountTaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class SingleAmountTaxScale(AmountTaxScaleLike): @@ -15,7 +15,7 @@ def calc( self, tax_base: NumericalArray, right: bool = False, - ) -> numpy.float64: + ) -> numpy.float32: """Matches the input amount to a set of brackets and returns the single cell value that fits within that bracket. """ diff --git a/openfisca_core/taxscales/tax_scale_like.py b/openfisca_core/taxscales/tax_scale_like.py index 683c77112..e8680b9f8 100644 --- a/openfisca_core/taxscales/tax_scale_like.py +++ b/openfisca_core/taxscales/tax_scale_like.py @@ -10,7 +10,7 @@ if typing.TYPE_CHECKING: import numpy - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class TaxScaleLike(abc.ABC): @@ -55,7 +55,7 @@ def calc( self, tax_base: NumericalArray, right: bool, - ) -> numpy.float64: ... + ) -> numpy.float32: ... @abc.abstractmethod def to_dict(self) -> dict: ... diff --git a/openfisca_core/tools/__init__.py b/openfisca_core/tools/__init__.py index 1416ed152..952dca6eb 100644 --- a/openfisca_core/tools/__init__.py +++ b/openfisca_core/tools/__init__.py @@ -1,7 +1,6 @@ import os -import numexpr - +from openfisca_core import commons from openfisca_core.indexed_enums import EnumArray @@ -33,7 +32,7 @@ def assert_near( target_value = numpy.array(target_value, dtype=value.dtype) assert_datetime_equals(value, target_value, message) if isinstance(target_value, str): - target_value = eval_expression(target_value) + target_value = commons.eval_expression(target_value) target_value = numpy.array(target_value).astype(numpy.float32) @@ -87,10 +86,3 @@ def get_trace_tool_link(scenario, variables, api_url, trace_tool_url): }, ) ) - - -def eval_expression(expression): - try: - return numexpr.evaluate(expression) - except (KeyError, TypeError): - return expression diff --git a/openfisca_core/tools/simulation_dumper.py b/openfisca_core/tools/simulation_dumper.py index 9b1f5708a..84898165f 100644 --- a/openfisca_core/tools/simulation_dumper.py +++ b/openfisca_core/tools/simulation_dumper.py @@ -81,7 +81,7 @@ def _dump_entity(population, directory) -> None: flattened_roles = population.entity.flattened_roles if len(flattened_roles) == 0: - encoded_roles = numpy.int64(0) + encoded_roles = numpy.int16(0) else: encoded_roles = numpy.select( [population.members_role == role for role in flattened_roles], @@ -106,7 +106,7 @@ def _restore_entity(population, directory): flattened_roles = population.entity.flattened_roles if len(flattened_roles) == 0: - population.members_role = numpy.int64(0) + population.members_role = numpy.int16(0) else: population.members_role = numpy.select( [encoded_roles == role.key for role in flattened_roles], diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 77411c32b..e1aa50108 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -11,7 +11,7 @@ import numpy import sortedcontainers -from openfisca_core import periods, tools +from openfisca_core import commons, periods from openfisca_core.entities import Entity, GroupEntity from openfisca_core.indexed_enums import Enum, EnumArray from openfisca_core.periods import DateUnit, Period @@ -452,7 +452,7 @@ def check_set_value(self, value): ) if self.value_type in (float, int) and isinstance(value, str): try: - value = tools.eval_expression(value) + value = commons.eval_expression(value) except SyntaxError: msg = f"I couldn't understand '{value}' as a value for '{self.name}'" raise ValueError( diff --git a/setup.cfg b/setup.cfg index 596ce9915..2d13ef286 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ in-place = true include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors max-line-length = 88 per-file-ignores = - */types.py:D101,D102,E704 + */types.py:D101,D102,E301,E704 */test_*.py:D101,D102,D103 */__init__.py:F401 */__init__.pyi:E302,E704 diff --git a/stubs/numexpr/__init__.pyi b/stubs/numexpr/__init__.pyi new file mode 100644 index 000000000..95a0f4f80 --- /dev/null +++ b/stubs/numexpr/__init__.pyi @@ -0,0 +1,9 @@ +from __future__ import annotations + +from numpy.typing import NDArray + +import numpy + +def evaluate( + __ex: str, *__args: object, **__kwargs: object +) -> NDArray[numpy.bool_]| NDArray[numpy.int32] | NDArray[numpy.float32]: ... From 5b9511fa6b25bacadaa7353798c075ce073c09e9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 19:16:03 +0200 Subject: [PATCH 05/10] build(isort): fix imports config --- openfisca_core/commons/misc.py | 2 +- openfisca_core/holders/holder.py | 3 +-- openfisca_core/parameters/config.py | 4 ++-- openfisca_tasks/lint.mk | 4 ++-- setup.cfg | 7 ++++++- stubs/numexpr/__init__.pyi | 2 +- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 99b830099..1678aace2 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -11,7 +11,7 @@ def empty_clone(original: T) -> T: - """Creates an empty instance of the same class of the original object. + """Create an empty instance of the same class of the original object. Args: original: An object to clone. diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index a8ddf3ed3..7183d4a44 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -15,7 +15,6 @@ errors, indexed_enums as enums, periods, - tools, types, ) @@ -235,7 +234,7 @@ def set_input( warning_message = f"You cannot set a value for the variable {self.variable.name}, as it has been neutralized. The value you provided ({array}) will be ignored." return warnings.warn(warning_message, Warning, stacklevel=2) if self.variable.value_type in (float, int) and isinstance(array, str): - array = tools.eval_expression(array) + array = commons.eval_expression(array) if self.variable.set_input: return self.variable.set_input(self, period, array) return self._set(period, array) diff --git a/openfisca_core/parameters/config.py b/openfisca_core/parameters/config.py index b97462a79..5fb1198be 100644 --- a/openfisca_core/parameters/config.py +++ b/openfisca_core/parameters/config.py @@ -15,8 +15,8 @@ "so that it is used in your Python environment." + os.linesep, ] warnings.warn(" ".join(message), LibYAMLWarning, stacklevel=2) - from yaml import ( - Loader, # type: ignore # (see https://github.com/python/mypy/issues/1153#issuecomment-455802270) + from yaml import ( # type: ignore # (see https://github.com/python/mypy/issues/1153#issuecomment-455802270) + Loader, ) # 'unit' and 'reference' are only listed here for backward compatibility. diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 7f90aa5d5..8afba8cd9 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -9,7 +9,7 @@ check-syntax-errors: . @$(call print_pass,$@:) ## Run linters to check for syntax and style errors. -check-style: $(shell git ls-files "*.py") +check-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) @isort --check $? @black --check $? @@ -47,7 +47,7 @@ check-types: @$(call print_pass,$@:) ## Run code formatters to correct style errors. -format-style: $(shell git ls-files "*.py") +format-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) @isort $? @black $? diff --git a/setup.cfg b/setup.cfg index 2d13ef286..7b826f818 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,12 @@ docstring_style = google extend-ignore = D ignore = B019, E203, E501, F405, E701, E704, RST212, RST301, W503 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors +include-in-doctest = + openfisca_core/commons + openfisca_core/entities + openfisca_core/holders + openfisca_core/periods + openfisca_core/projectors max-line-length = 88 per-file-ignores = */types.py:D101,D102,E301,E704 diff --git a/stubs/numexpr/__init__.pyi b/stubs/numexpr/__init__.pyi index 95a0f4f80..6607dd9ee 100644 --- a/stubs/numexpr/__init__.pyi +++ b/stubs/numexpr/__init__.pyi @@ -6,4 +6,4 @@ import numpy def evaluate( __ex: str, *__args: object, **__kwargs: object -) -> NDArray[numpy.bool_]| NDArray[numpy.int32] | NDArray[numpy.float32]: ... +) -> NDArray[numpy.bool_] | NDArray[numpy.int32] | NDArray[numpy.float32]: ... From 176afe37cdbc534e4d1e9015ca0cbdca27292ac7 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 19:27:28 +0200 Subject: [PATCH 06/10] doc(commons): add doctest to eval_expression --- openfisca_core/commons/misc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 1678aace2..e9ca604e8 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -103,6 +103,7 @@ def eval_expression( try: return numexpr.evaluate(expression) + except (KeyError, TypeError): return expression From 277bd75cd0a31213d55c0bfd0c9dc46e1e2b492a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 20 Sep 2024 19:19:43 +0200 Subject: [PATCH 07/10] refactor(commons): fix linter warnings --- openfisca_core/commons/formulas.py | 2 +- openfisca_core/commons/misc.py | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index eb49a9f52..478182174 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -59,7 +59,7 @@ def concat( this: t.Array[numpy.str_] | t.ArrayLike[str], that: t.Array[numpy.str_] | t.ArrayLike[str], ) -> t.Array[numpy.str_]: - """Concatenates the values of two arrays. + """Concatenate the values of two arrays. Args: this: An array to concatenate. diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index e9ca604e8..138dd18e1 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,16 +1,12 @@ from __future__ import annotations -from typing import TypeVar - import numexpr import numpy from openfisca_core import types as t -T = TypeVar("T") - -def empty_clone(original: T) -> T: +def empty_clone(original: object) -> object: """Create an empty instance of the same class of the original object. Args: @@ -33,13 +29,11 @@ def empty_clone(original: T) -> T: True """ - Dummy: object - new: T Dummy = type( "Dummy", (original.__class__,), - {"__init__": lambda self: None}, + {"__init__": lambda _: None}, ) new = Dummy() @@ -48,7 +42,7 @@ def empty_clone(original: T) -> T: def stringify_array(array: None | t.Array[numpy.generic]) -> str: - """Generates a clean string representation of a numpy array. + """Generate a clean string representation of a numpy array. Args: array: An array. From 52a2584c3209d71498871d9911913eb99d7c330f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 25 Sep 2024 18:05:47 +0200 Subject: [PATCH 08/10] fix(commons): doc line length --- openfisca_core/commons/formulas.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 478182174..5232c3ba4 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -48,9 +48,11 @@ def apply_thresholds( # must be true to return it. condlist += [True] - assert len(condlist) == len( - choices - ), "'apply_thresholds' must be called with the same number of thresholds than choices, or one more choice." + msg = ( + "'apply_thresholds' must be called with the same number of thresholds " + "than choices, or one more choice." + ) + assert len(condlist) == len(choices), msg return numpy.select(condlist, choices) From da1139e4b61d9a5a4b8a1e0a9cebc8b5a36fcede Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 25 Sep 2024 20:32:53 +0200 Subject: [PATCH 09/10] fix(entities): types --- openfisca_core/entities/_core_entity.py | 49 ++++++++----- openfisca_core/entities/_description.py | 55 +++++++++++++++ openfisca_core/entities/entity.py | 13 +++- openfisca_core/entities/group_entity.py | 16 +++-- openfisca_core/entities/helpers.py | 16 +++-- openfisca_core/entities/role.py | 89 ++++++------------------ openfisca_core/entities/types.py | 69 ++++++++----------- openfisca_core/types.py | 92 +++++++++++++++---------- openfisca_tasks/lint.mk | 1 - setup.cfg | 2 +- setup.py | 4 +- stubs/numexpr/__init__.pyi | 6 +- 12 files changed, 223 insertions(+), 189 deletions(-) create mode 100644 openfisca_core/entities/_description.py diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index da3e6ea98..ca3553379 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -1,7 +1,9 @@ from __future__ import annotations +from typing import ClassVar + +import abc import os -from abc import abstractmethod from . import types as t from .role import Role @@ -12,29 +14,30 @@ class _CoreEntity: #: A key to identify the entity. key: t.EntityKey + #: The ``key``, pluralised. - plural: t.EntityPlural | None + plural: t.EntityPlural #: A summary description. - label: str | None + label: str #: A full description. - doc: str | None + doc: str #: Whether the entity is a person or not. - is_person: bool + is_person: ClassVar[bool] #: A TaxBenefitSystem instance. - _tax_benefit_system: t.TaxBenefitSystem | None = None + _tax_benefit_system: None | t.TaxBenefitSystem = None - @abstractmethod + @abc.abstractmethod def __init__( self, - key: str, - plural: str, - label: str, - doc: str, - *args: object, + __key: str, + __plural: str, + __label: str, + __doc: str, + *__args: object, ) -> None: ... def __repr__(self) -> str: @@ -46,7 +49,7 @@ def set_tax_benefit_system(self, tax_benefit_system: t.TaxBenefitSystem) -> None def get_variable( self, - variable_name: str, + variable_name: t.VariableName, check_existence: bool = False, ) -> t.Variable | None: """Get a ``variable_name`` from ``variables``.""" @@ -57,16 +60,20 @@ def get_variable( ) return self._tax_benefit_system.get_variable(variable_name, check_existence) - def check_variable_defined_for_entity(self, variable_name: str) -> None: + def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> None: """Check if ``variable_name`` is defined for ``self``.""" - variable: t.Variable | None - entity: t.CoreEntity - - variable = self.get_variable(variable_name, check_existence=True) + entity: None | t.CoreEntity = None + variable: None | t.Variable = self.get_variable( + variable_name, + check_existence=True, + ) if variable is not None: entity = variable.entity + if entity is None: + return + if entity.key != self.key: message = ( f"You tried to compute the variable '{variable_name}' for", @@ -77,8 +84,12 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None: ) raise ValueError(os.linesep.join(message)) - def check_role_validity(self, role: object) -> None: + @staticmethod + def check_role_validity(role: object) -> None: """Check if a ``role`` is an instance of Role.""" if role is not None and not isinstance(role, Role): msg = f"{role} is not a valid role" raise ValueError(msg) + + +__all__ = ["_CoreEntity"] diff --git a/openfisca_core/entities/_description.py b/openfisca_core/entities/_description.py new file mode 100644 index 000000000..6e0a62beb --- /dev/null +++ b/openfisca_core/entities/_description.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import dataclasses +import textwrap + + +@dataclasses.dataclass(frozen=True) +class _Description: + r"""A Role's description. + + Examples: + >>> data = { + ... "key": "parent", + ... "label": "Parents", + ... "plural": "parents", + ... "doc": "\t\t\tThe one/two adults in charge of the household.", + ... } + + >>> description = _Description(**data) + + >>> repr(_Description) + "" + + >>> repr(description) + "_Description(key='parent', plural='parents', label='Parents', ...)" + + >>> str(description) + "_Description(key='parent', plural='parents', label='Parents', ...)" + + >>> {description} + {_Description(key='parent', plural='parents', label='Parents', doc=...} + + >>> description.key + 'parent' + + """ + + #: A key to identify the Role. + key: str + + #: The ``key``, pluralised. + plural: None | str = None + + #: A summary description. + label: None | str = None + + #: A full description, non-indented. + doc: None | str = None + + def __post_init__(self) -> None: + if self.doc is not None: + object.__setattr__(self, "doc", textwrap.dedent(self.doc)) + + +__all__ = ["_Description"] diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index a3fbaddac..3b1a6713e 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,3 +1,5 @@ +from typing import ClassVar + import textwrap from . import types as t @@ -5,11 +7,16 @@ class Entity(_CoreEntity): - """Represents an entity (e.g. a person, a household, etc.) on which calculations can be run.""" + """An entity (e.g. a person, a household) on which calculations can be run.""" + + #: Whether the entity is a person or not. + is_person: ClassVar[bool] = True def __init__(self, key: str, plural: str, label: str, doc: str) -> None: self.key = t.EntityKey(key) - self.label = label self.plural = t.EntityPlural(plural) + self.label = label self.doc = textwrap.dedent(doc) - self.is_person = True + + +__all__ = ["Entity"] diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index b47c92a52..5ae74de56 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections.abc import Iterable, Sequence +from typing import ClassVar import textwrap from itertools import chain @@ -28,6 +29,9 @@ class GroupEntity(_CoreEntity): """ + #: Whether the entity is a person or not. + is_person: ClassVar[bool] = False + def __init__( self, key: str, @@ -38,19 +42,18 @@ def __init__( containing_entities: Iterable[str] = (), ) -> None: self.key = t.EntityKey(key) - self.label = label self.plural = t.EntityPlural(plural) + self.label = label self.doc = textwrap.dedent(doc) - self.is_person = False self.roles_description = roles self.roles: Iterable[Role] = () for role_description in roles: role = Role(role_description, self) setattr(self, role.key.upper(), role) self.roles = (*self.roles, role) - if role_description.get("subroles"): + if subroles := role_description.get("subroles"): role.subroles = () - for subrole_key in role_description["subroles"]: + for subrole_key in subroles: subrole = Role({"key": subrole_key, "max": 1}, self) setattr(self, subrole.key.upper(), subrole) role.subroles = (*role.subroles, subrole) @@ -58,6 +61,7 @@ def __init__( self.flattened_roles = tuple( chain.from_iterable(role.subroles or [role] for role in self.roles), ) - - self.is_person = False self.containing_entities = containing_entities + + +__all__ = ["GroupEntity"] diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index d5ba6cc6a..808ff0e61 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -3,7 +3,7 @@ from collections.abc import Iterable, Sequence from . import types as t -from .entity import Entity +from .entity import Entity as SingleEntity from .group_entity import GroupEntity @@ -12,9 +12,9 @@ def build_entity( plural: str, label: str, doc: str = "", - roles: Sequence[t.RoleParams] | None = None, + roles: None | Sequence[t.RoleParams] = None, is_person: bool = False, - class_override: object | None = None, + class_override: object = None, containing_entities: Sequence[str] = (), ) -> t.SingleEntity | t.GroupEntity: """Build a SingleEntity or a GroupEntity. @@ -69,8 +69,9 @@ def build_entity( TypeError: 'Role' object is not iterable """ + if is_person: - return Entity(key, plural, label, doc) + return SingleEntity(key, plural, label, doc) if roles is not None: return GroupEntity( @@ -89,8 +90,8 @@ def find_role( roles: Iterable[t.Role], key: t.RoleKey, *, - total: int | None = None, -) -> t.Role | None: + total: None | int = None, +) -> None | t.Role: """Find a Role in a GroupEntity. Args: @@ -157,3 +158,6 @@ def find_role( return role return None + + +__all__ = ["build_entity", "find_role"] diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 45193fffc..0cb83cde5 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,12 +1,9 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping -from typing import Any +from collections.abc import Iterable -import dataclasses -import textwrap - -from .types import SingleEntity +from . import types as t +from ._description import _Description class Role: @@ -48,44 +45,45 @@ class Role: """ #: The Entity the Role belongs to. - entity: SingleEntity + entity: t.GroupEntity #: A description of the Role. description: _Description #: Max number of members. - max: int | None = None + max: None | int = None #: A list of subroles. - subroles: Iterable[Role] | None = None + subroles: None | Iterable[Role] = None @property - def key(self) -> str: + def key(self) -> t.RoleKey: """A key to identify the Role.""" - return self.description.key + return t.RoleKey(self.description.key) @property - def plural(self) -> str | None: + def plural(self) -> None | t.RolePlural: """The ``key``, pluralised.""" - return self.description.plural + if (plural := self.description.plural) is None: + return None + return t.RolePlural(plural) @property - def label(self) -> str | None: + def label(self) -> None | str: """A summary description.""" return self.description.label @property - def doc(self) -> str | None: + def doc(self) -> None | str: """A full description, non-indented.""" return self.description.doc - def __init__(self, description: Mapping[str, Any], entity: SingleEntity) -> None: + def __init__(self, description: t.RoleParams, entity: t.GroupEntity) -> None: self.description = _Description( - **{ - key: value - for key, value in description.items() - if key in {"key", "plural", "label", "doc"} - }, + key=description["key"], + plural=description.get("plural"), + label=description.get("label"), + doc=description.get("doc"), ) self.entity = entity self.max = description.get("max") @@ -94,51 +92,4 @@ def __repr__(self) -> str: return f"Role({self.key})" -@dataclasses.dataclass(frozen=True) -class _Description: - r"""A Role's description. - - Examples: - >>> data = { - ... "key": "parent", - ... "label": "Parents", - ... "plural": "parents", - ... "doc": "\t\t\tThe one/two adults in charge of the household.", - ... } - - >>> description = _Description(**data) - - >>> repr(_Description) - "" - - >>> repr(description) - "_Description(key='parent', plural='parents', label='Parents', ...)" - - >>> str(description) - "_Description(key='parent', plural='parents', label='Parents', ...)" - - >>> {description} - {_Description(key='parent', plural='parents', label='Parents', doc=...} - - >>> description.key - 'parent' - - .. versionadded:: 41.0.1 - - """ - - #: A key to identify the Role. - key: str - - #: The ``key``, pluralised. - plural: str | None = None - - #: A summary description. - label: str | None = None - - #: A full description, non-indented. - doc: str | None = None - - def __post_init__(self) -> None: - if self.doc is not None: - object.__setattr__(self, "doc", textwrap.dedent(self.doc)) +__all__ = ["Role"] diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index 38607d548..ef6af9024 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -1,40 +1,21 @@ -from __future__ import annotations - -from collections.abc import Iterable -from typing import NewType, Protocol from typing_extensions import Required, TypedDict -from openfisca_core import types as t +from openfisca_core.types import ( + CoreEntity, + EntityKey, + EntityPlural, + GroupEntity, + Role, + RoleKey, + RolePlural, + SingleEntity, + TaxBenefitSystem, + Variable, + VariableName, +) # Entities -#: For example "person". -EntityKey = NewType("EntityKey", str) - -#: For example "persons". -EntityPlural = NewType("EntityPlural", str) - -#: For example "principal". -RoleKey = NewType("RoleKey", str) - -#: For example "parents". -RolePlural = NewType("RolePlural", str) - - -class CoreEntity(t.CoreEntity, Protocol): - key: EntityKey - plural: EntityPlural | None - - -class SingleEntity(t.SingleEntity, Protocol): ... - - -class GroupEntity(t.GroupEntity, Protocol): ... - - -class Role(t.Role, Protocol): - subroles: Iterable[Role] | None - class RoleParams(TypedDict, total=False): key: Required[str] @@ -45,13 +26,17 @@ class RoleParams(TypedDict, total=False): subroles: list[str] -# Tax-Benefit systems - - -class TaxBenefitSystem(t.TaxBenefitSystem, Protocol): ... - - -# Variables - - -class Variable(t.Variable, Protocol): ... +__all__ = [ + "CoreEntity", + "EntityKey", + "EntityPlural", + "GroupEntity", + "Role", + "RoleKey", + "RoleParams", + "RolePlural", + "SingleEntity", + "TaxBenefitSystem", + "Variable", + "VariableName", +] diff --git a/openfisca_core/types.py b/openfisca_core/types.py index 5134c518d..d1c13c1f1 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -2,46 +2,56 @@ from collections.abc import Iterable, Sequence, Sized from numpy.typing import NDArray -from typing import Any, TypeVar, Union +from typing import Any, NewType, TypeVar, Union from typing_extensions import Protocol, TypeAlias import numpy -N = TypeVar("N", bound=numpy.generic, covariant=True) +_N_co = TypeVar("_N_co", bound=numpy.generic, covariant=True) #: Type representing an numpy array. -Array: TypeAlias = NDArray[N] +Array: TypeAlias = NDArray[_N_co] 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) - #: Generic type vars. -T_cov = TypeVar("T_cov", covariant=True) -T_con = TypeVar("T_con", contravariant=True) +_T_co = TypeVar("_T_co", covariant=True) # Entities +#: For example "person". +EntityKey = NewType("EntityKey", str) + +#: For example "persons". +EntityPlural = NewType("EntityPlural", str) + +#: For example "principal". +RoleKey = NewType("RoleKey", str) + +#: For example "parents". +RolePlural = NewType("RolePlural", str) + class CoreEntity(Protocol): - key: Any - plural: Any + key: EntityKey + plural: EntityPlural - def check_role_validity(self, role: Any) -> None: ... - def check_variable_defined_for_entity(self, variable_name: Any) -> None: ... + def check_role_validity(self, role: object, /) -> None: ... + def check_variable_defined_for_entity( + self, + variable_name: VariableName, + /, + ) -> None: ... def get_variable( self, - variable_name: Any, - check_existence: Any = ..., - ) -> Any | None: ... + variable_name: VariableName, + check_existence: bool = ..., + /, + ) -> None | Variable: ... class SingleEntity(CoreEntity, Protocol): ... @@ -51,20 +61,22 @@ class GroupEntity(CoreEntity, Protocol): ... class Role(Protocol): - entity: Any + entity: GroupEntity max: int | None - subroles: Any + subroles: None | Iterable[Role] @property - def key(self) -> str: ... + def key(self, /) -> RoleKey: ... + @property + def plural(self, /) -> None | RolePlural: ... # Holders class Holder(Protocol): - def clone(self, population: Any) -> Holder: ... - def get_memory_usage(self) -> Any: ... + def clone(self, population: Any, /) -> Holder: ... + def get_memory_usage(self, /) -> Any: ... # Parameters @@ -76,12 +88,12 @@ class ParameterNodeAtInstant(Protocol): ... # Periods -class Container(Protocol[T_con]): - def __contains__(self, item: T_con, /) -> bool: ... +class Container(Protocol[_T_co]): + def __contains__(self, item: object, /) -> bool: ... -class Indexable(Protocol[T_cov]): - def __getitem__(self, index: int, /) -> T_cov: ... +class Indexable(Protocol[_T_co]): + def __getitem__(self, index: int, /) -> _T_co: ... class DateUnit(Container[str], Protocol): ... @@ -92,7 +104,7 @@ class Instant(Indexable[int], Iterable[int], Sized, Protocol): ... class Period(Indexable[Union[DateUnit, Instant, int]], Protocol): @property - def unit(self) -> DateUnit: ... + def unit(self, /) -> DateUnit: ... # Populations @@ -101,17 +113,17 @@ def unit(self) -> DateUnit: ... class Population(Protocol): entity: Any - def get_holder(self, variable_name: Any) -> Any: ... + def get_holder(self, variable_name: VariableName, /) -> Any: ... # Simulations class Simulation(Protocol): - def calculate(self, variable_name: Any, period: Any) -> Any: ... - def calculate_add(self, variable_name: Any, period: Any) -> Any: ... - def calculate_divide(self, variable_name: Any, period: Any) -> Any: ... - def get_population(self, plural: Any | None) -> Any: ... + def calculate(self, variable_name: VariableName, period: Any, /) -> Any: ... + def calculate_add(self, variable_name: VariableName, period: Any, /) -> Any: ... + def calculate_divide(self, variable_name: VariableName, period: Any, /) -> Any: ... + def get_population(self, plural: None | str, /) -> Any: ... # Tax-Benefit systems @@ -122,16 +134,21 @@ class TaxBenefitSystem(Protocol): def get_variable( self, - variable_name: Any, - check_existence: Any = ..., - ) -> Any | None: ... + variable_name: VariableName, + check_existence: bool = ..., + /, + ) -> None | Variable: ... # Variables +#: For example "salary". +VariableName = NewType("VariableName", str) + class Variable(Protocol): entity: Any + name: VariableName class Formula(Protocol): @@ -140,8 +157,9 @@ def __call__( population: Population, instant: Instant, params: Params, + /, ) -> Array[Any]: ... class Params(Protocol): - def __call__(self, instant: Instant) -> ParameterNodeAtInstant: ... + def __call__(self, instant: Instant, /) -> ParameterNodeAtInstant: ... diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 8afba8cd9..99b5cba7b 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -42,7 +42,6 @@ check-types: @mypy \ openfisca_core/commons \ openfisca_core/entities \ - openfisca_core/periods \ openfisca_core/types.py @$(call print_pass,$@:) diff --git a/setup.cfg b/setup.cfg index 7b826f818..c31d9f9e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ include-in-doctest = openfisca_core/projectors max-line-length = 88 per-file-ignores = - */types.py:D101,D102,E301,E704 + */types.py:D101,D102,E301,E704,W504 */test_*.py:D101,D102,D103 */__init__.py:F401 */__init__.pyi:E302,E704 diff --git a/setup.py b/setup.py index 6b573a28b..ff2b0c430 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 >=3.0.0, <4.0.0", + "pendulum >=2.1.2, <3.0.0", "psutil >=5.9.4, <6.0", "pytest >=8.3.3, <9.0", "sortedcontainers >=2.4.0, <3.0", @@ -61,7 +61,7 @@ "openapi-spec-validator >=0.7.1, <0.8.0", "pylint >=3.3.1, <4.0", "pylint-per-file-ignores >=1.3.2, <2.0", - "pyright >=1.1.381, <2.0", + "pyright >=1.1.382, <2.0", "ruff >=0.6.7, <1.0", "ruff-lsp >=0.0.57, <1.0", "xdoctest >=1.2.0, <2.0", diff --git a/stubs/numexpr/__init__.pyi b/stubs/numexpr/__init__.pyi index 6607dd9ee..f9ada73c3 100644 --- a/stubs/numexpr/__init__.pyi +++ b/stubs/numexpr/__init__.pyi @@ -1,9 +1,9 @@ -from __future__ import annotations - from numpy.typing import NDArray import numpy def evaluate( - __ex: str, *__args: object, **__kwargs: object + __ex: str, + *__args: object, + **__kwargs: object, ) -> NDArray[numpy.bool_] | NDArray[numpy.int32] | NDArray[numpy.float32]: ... From ecf904f4d4943e5742ca92627c48fd7b94219a8e Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 19:40:50 +0200 Subject: [PATCH 10/10] chore(version): bump --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 002cf1471..93e867efe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.5.7 [#1225](https://github.com/openfisca/openfisca-core/pull/1225) + +#### Technical changes + +- Refactor & test `eval_expression` + ### 41.5.6 [#1185](https://github.com/openfisca/openfisca-core/pull/1185) #### Technical changes diff --git a/setup.py b/setup.py index ff2b0c430..9b62476a4 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="OpenFisca-Core", - version="41.5.6", + version="41.5.7", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[