diff --git a/CHANGELOG.md b/CHANGELOG.md index 75fdd9ed3f..5a68701ff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +### 41.5.4 [#1219](https://github.com/openfisca/openfisca-core/pull/1219) + +#### Technical changes + +- Fix optional type-checks for: + - Commons + - Entities + - Projectors + - Simulations (partial) + ### 41.5.3 [#1218](https://github.com/openfisca/openfisca-core/pull/1218) #### Technical changes diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index b3b5d8cbb2..7c5c89e3e5 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -4,6 +4,7 @@ to OpenFisca Core and to country packages. Official Public API: + * :class:`.either` * :func:`.apply_thresholds` * :func:`.average_rate` * :func:`.concat` @@ -50,18 +51,16 @@ """ -# Official Public API +from ._adts import Either, Failure, Success +from .dummy import Dummy # Deprecated +from .formulas import apply_thresholds, concat, switch +from .misc import empty_clone, stringify_array +from .rates import average_rate, marginal_rate -from .formulas import apply_thresholds, concat, switch # noqa: F401 -from .misc import empty_clone, stringify_array # noqa: F401 -from .rates import average_rate, marginal_rate # noqa: F401 +either = Either __all__ = ["apply_thresholds", "concat", "switch"] __all__ = ["empty_clone", "stringify_array", *__all__] __all__ = ["average_rate", "marginal_rate", *__all__] - -# Deprecated - -from .dummy import Dummy # noqa: F401 - +__all__ = ["Either", "Failure", "Success", "either", *__all__] __all__ = ["Dummy", *__all__] diff --git a/openfisca_core/commons/_adts.py b/openfisca_core/commons/_adts.py new file mode 100644 index 0000000000..4b7c073026 --- /dev/null +++ b/openfisca_core/commons/_adts.py @@ -0,0 +1,116 @@ +"""Algebraic data types for OpenFisca. + +An algebraic data type is a structured type that’s formed by composing other +types. [...] Product types allow you to have more than one value in a single +structure, at the same time. [...] Sum types are types where your value must +be one of a fixed set of options. + +.. _See: + https://jrsinclair.com/articles/2019/algebraic-data-types-what-i-wish-someone-had-explained-about-functional-programming/ + +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import Generic, TypeVar, cast, final +from typing_extensions import Never + +import dataclasses + +#: Type variable representing an error. +E = TypeVar("E") + +#: Type variable representing a value. +A = TypeVar("A") + + +@dataclasses.dataclass(frozen=True) +class Either(Generic[E, A]): + """The Either monad. + + The Either monad specifies the Either data type as well as several + functions that operate on top of it. The Either data type represents the + result of a computation that may fail. + + """ + + #: The value or state passed on. + _value: E | A + + @property + @final + def is_failure(self) -> bool: + """bool: Whether this instance represents a failure.""" + return isinstance(self, Failure) + + @property + @final + def is_success(self) -> bool: + """bool: Whether this instance represents a success.""" + return isinstance(self, Success) + + @final + def unwrap(self) -> E | A: + """Return the value of this instance. + + Examples: + >>> Either.fail("error").unwrap() + 'error' + + >>> Either.succeed(1).unwrap() + 1 + + Returns: + E | A: The value of this instance. + + """ + + return self._value + + @final + def then( + self, f: Callable[[A], Failure[E] | Success[A]] + ) -> Failure[E] | Success[A]: + """Apply a flatMap to input stream. + + Examples: + >>> Either.fail("error").then(lambda x: Either.succeed(x)).unwrap() + 'error' + + >>> Either.succeed(1).then(lambda x: Either.succeed(x + 1)).unwrap() + 2 + + Args: + f: A function that takes a value and returns a new Either instance. + + Returns: + _Failure[E] | _Success[A]: The result of applying f. + + """ + + if self.is_success: + return f(cast(A, self.unwrap())) + return Either.fail(cast(E, self.unwrap())) + + @staticmethod + @final + def fail(value: E) -> Failure[E]: + """_Failure[E]: Create a failing result.""" + return Failure(value) + + @staticmethod + @final + def succeed(value: A) -> Success[A]: + """_Success[A]: Create a successful result.""" + return Success(value) + + +@final +class Failure(Either[E, Never]): + """A failing result in an Either ADT.""" + + +@final +class Success(Either[Never, A]): + """A successful result in an Either ADT.""" diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index bbcc4fe565..c1cc159e50 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,17 +1,15 @@ -from typing import Any, Dict, Sequence, TypeVar +from typing import Any, Union from openfisca_core.types import Array, ArrayLike import numpy -T = TypeVar("T") - def apply_thresholds( - input: Array[float], + input: Array[numpy.float_], thresholds: ArrayLike[float], choices: ArrayLike[float], -) -> Array[float]: +) -> Array[numpy.float_]: """Makes a choice based on an input and thresholds. From a list of ``choices``, this function selects one of these values @@ -40,7 +38,7 @@ def apply_thresholds( """ - condlist: Sequence[Array[bool]] + condlist: list[Union[Array[numpy.bool_], bool]] condlist = [input <= threshold for threshold in thresholds] if len(condlist) == len(choices) - 1: @@ -58,7 +56,9 @@ def apply_thresholds( return numpy.select(condlist, choices) -def concat(this: ArrayLike[str], that: ArrayLike[str]) -> Array[str]: +def concat( + this: Union[Array[Any], ArrayLike[str]], that: Union[Array[Any], ArrayLike[str]] +) -> Array[numpy.str_]: """Concatenates the values of two arrays. Args: @@ -88,8 +88,8 @@ def concat(this: ArrayLike[str], that: ArrayLike[str]) -> Array[str]: def switch( conditions: Array[Any], - value_by_condition: Dict[float, T], -) -> Array[T]: + value_by_condition: dict[float, Any], +) -> Array[Any]: """Mimicks a switch statement. Given an array of conditions, returns an array of the same size, @@ -120,4 +120,4 @@ def switch( condlist = [conditions == condition for condition in value_by_condition.keys()] - return numpy.select(condlist, value_by_condition.values()) + return numpy.select(condlist, tuple(value_by_condition.values())) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index ee985071bf..9c7fcc68fd 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,4 +1,4 @@ -from typing import TypeVar +from typing import Any, TypeVar, Union from openfisca_core.types import Array @@ -43,7 +43,7 @@ def empty_clone(original: T) -> T: return new -def stringify_array(array: Array) -> str: +def stringify_array(array: Union[Array[Any], None]) -> str: """Generates a clean string representation of a numpy array. Args: diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index 7ede496f8c..6df1f1fee2 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -6,10 +6,10 @@ def average_rate( - target: Array[float], + target: Array[numpy.float_], varying: ArrayLike[float], trim: Optional[ArrayLike[float]] = None, -) -> Array[float]: +) -> Array[numpy.float_]: """Computes the average rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -41,7 +41,7 @@ def average_rate( """ - average_rate: Array[float] + average_rate: Array[numpy.float_] average_rate = 1 - target / varying @@ -62,10 +62,10 @@ def average_rate( def marginal_rate( - target: Array[float], - varying: Array[float], + target: Array[numpy.float_], + varying: Array[numpy.float_], trim: Optional[ArrayLike[float]] = None, -) -> Array[float]: +) -> Array[numpy.float_]: """Computes the marginal rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -97,7 +97,7 @@ def marginal_rate( """ - marginal_rate: Array[float] + marginal_rate: Array[numpy.float_] marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) diff --git a/openfisca_core/commons/tests/test_adts.py b/openfisca_core/commons/tests/test_adts.py new file mode 100644 index 0000000000..9ff64d778c --- /dev/null +++ b/openfisca_core/commons/tests/test_adts.py @@ -0,0 +1,33 @@ +import pytest + +from openfisca_core import commons + + +@pytest.fixture +def failure(): + return commons.either.fail("error") + + +@pytest.fixture +def success(): + return commons.either.succeed(1) + + +def test_either_is_failure(failure): + assert failure.is_failure + + +def test_either_is_success(success): + assert success.is_success + + +def test_either_unwrap(failure): + assert failure.unwrap() == "error" + + +def test_either_then(failure, success): + assert failure.then(lambda x: failure).unwrap() == "error" + assert failure.then(lambda x: success).unwrap() == "error" + assert success.then(lambda x: failure).unwrap() == "error" + assert success.then(lambda x: success).unwrap() == 1 + assert success.then(lambda x: commons.either.succeed(x + 1)).unwrap() == 2 diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index a1cd397a3a..9546773cb8 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -27,4 +27,14 @@ from .helpers import build_entity, find_role from .role import Role -__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "find_role", "types"] +SingleEntity = Entity + +__all__ = [ + "Entity", + "SingleEntity", + "GroupEntity", + "Role", + "build_entity", + "find_role", + "types", +] diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index ecd17becae..89bd28eaf3 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -8,7 +8,7 @@ from abc import abstractmethod from .role import Role -from .types import Entity +from .types import CoreEntity class _CoreEntity: @@ -49,12 +49,16 @@ def get_variable( check_existence: bool = False, ) -> Variable | None: """Get a ``variable_name`` from ``variables``.""" + if self._tax_benefit_system is None: + raise ValueError( + "You must set 'tax_benefit_system' before calling this method." + ) return self._tax_benefit_system.get_variable(variable_name, check_existence) def check_variable_defined_for_entity(self, variable_name: str) -> None: """Check if ``variable_name`` is defined for ``self``.""" variable: Variable | None - entity: Entity + entity: CoreEntity variable = self.get_variable(variable_name, check_existence=True) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index a4cb75a860..d703578160 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -6,7 +6,7 @@ import dataclasses import textwrap -from .types import Entity +from .types import SingleEntity class Role: @@ -48,7 +48,7 @@ class Role: """ #: The Entity the Role belongs to. - entity: Entity + entity: SingleEntity #: A description of the Role. description: _Description @@ -79,7 +79,7 @@ def doc(self) -> str | None: """A full description, non-indented.""" return self.description.doc - def __init__(self, description: Mapping[str, Any], entity: Entity) -> None: + def __init__(self, description: Mapping[str, Any], entity: SingleEntity) -> None: self.description = _Description( **{ key: value diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index 1a2fb06b2c..df3a6fcea1 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -1,25 +1,30 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Protocol, TypedDict +from typing import Protocol +from typing_extensions import TypedDict +from openfisca_core import types as t -class Entity(Protocol): +# Entities + + +class CoreEntity(t.CoreEntity, Protocol): ... -class GroupEntity(Protocol): +class SingleEntity(t.SingleEntity, Protocol): + key: str + plural: str | None + + +class GroupEntity(t.GroupEntity, Protocol): ... -class Role(Protocol): - max: int | None +class Role(t.Role, Protocol): subroles: Iterable[Role] | None - @property - def key(self) -> str: - ... - class RoleParams(TypedDict, total=False): key: str diff --git a/openfisca_core/holders/helpers.py b/openfisca_core/holders/helpers.py index 0e88964fc7..aba3a054a8 100644 --- a/openfisca_core/holders/helpers.py +++ b/openfisca_core/holders/helpers.py @@ -21,7 +21,7 @@ def set_input_dispatch_by_period(holder, period, array): period_unit = period.unit if holder.variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar + periods.ISOFORMAT + periods.ISOCALENDAR ): raise ValueError( "set_input_dispatch_by_period can't be used for eternal variables." @@ -57,7 +57,7 @@ def set_input_divide_by_period(holder, period, array): period_unit = period.unit if holder.variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar + periods.ISOFORMAT + periods.ISOCALENDAR ): raise ValueError( "set_input_divide_by_period can't be used for eternal variables." diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 4669c7ff4f..ca23dbf765 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -21,20 +21,14 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .config import ( # noqa: F401 - DAY, - ETERNITY, +from .config import ( INSTANT_PATTERN, - MONTH, - WEEK, - WEEKDAY, - YEAR, date_by_instant_cache, str_by_instant_cache, year_or_month_or_day_re, ) -from .date_unit import DateUnit # noqa: F401 -from .helpers import ( # noqa: F401 +from .date_unit import DateUnit +from .helpers import ( instant, instant_date, key_period_size, @@ -42,5 +36,38 @@ unit_weight, unit_weights, ) -from .instant_ import Instant # noqa: F401 -from .period_ import Period # noqa: F401 +from .instant_ import Instant +from .period_ import Period + +WEEKDAY = DateUnit.WEEKDAY +WEEK = DateUnit.WEEK +DAY = DateUnit.DAY +MONTH = DateUnit.MONTH +YEAR = DateUnit.YEAR +ETERNITY = DateUnit.ETERNITY +ISOFORMAT = DateUnit.isoformat +ISOCALENDAR = DateUnit.isocalendar + +__all__ = [ + "INSTANT_PATTERN", + "date_by_instant_cache", + "str_by_instant_cache", + "year_or_month_or_day_re", + "DateUnit", + "instant", + "instant_date", + "key_period_size", + "period", + "unit_weight", + "unit_weights", + "Instant", + "Period", + "WEEKDAY", + "WEEK", + "DAY", + "MONTH", + "YEAR", + "ETERNITY", + "ISOFORMAT", + "ISOCALENDAR", +] diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/config.py index 17807160e4..afbfbd9d0d 100644 --- a/openfisca_core/periods/config.py +++ b/openfisca_core/periods/config.py @@ -1,14 +1,5 @@ import re -from .date_unit import DateUnit - -WEEKDAY = DateUnit.WEEKDAY -WEEK = DateUnit.WEEK -DAY = DateUnit.DAY -MONTH = DateUnit.MONTH -YEAR = DateUnit.YEAR -ETERNITY = DateUnit.ETERNITY - # Matches "2015", "2015-01", "2015-01-01" # Does not match "2015-13", "2015-12-32" INSTANT_PATTERN = re.compile( diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 11a7b671b4..211b7387ca 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -269,14 +269,14 @@ def size_in_years(self) -> int: >>> period = Period((DateUnit.MONTH, instant, 3)) >>> period.size_in_years Traceback (most recent call last): - ValueError: Can't calculate number of years in a month. + ValueError: Cannot calculate number of years in month. """ if self.unit == DateUnit.YEAR: return self.size - raise ValueError(f"Can't calculate number of years in a {self.unit}.") + raise ValueError(f"Cannot calculate number of years in {self.unit}.") @property def size_in_months(self) -> int: @@ -292,7 +292,7 @@ def size_in_months(self) -> int: >>> period = Period((DateUnit.DAY, instant, 3)) >>> period.size_in_months Traceback (most recent call last): - ValueError: Can't calculate number of months in a day. + ValueError: Cannot calculate number of months in day. """ @@ -302,7 +302,7 @@ def size_in_months(self) -> int: if self.unit == DateUnit.MONTH: return self.size - raise ValueError(f"Can't calculate number of months in a {self.unit}.") + raise ValueError(f"Cannot calculate number of months in {self.unit}.") @property def size_in_days(self) -> int: @@ -331,7 +331,7 @@ def size_in_days(self) -> int: if self.unit in (DateUnit.DAY, DateUnit.WEEKDAY): return self.size - raise ValueError(f"Can't calculate number of days in a {self.unit}.") + raise ValueError(f"Cannot calculate number of days in {self.unit}.") @property def size_in_weeks(self): @@ -365,7 +365,7 @@ def size_in_weeks(self): if self.unit == DateUnit.WEEK: return self.size - raise ValueError(f"Can't calculate number of weeks in a {self.unit}.") + raise ValueError(f"Cannot calculate number of weeks in {self.unit}.") @property def size_in_weekdays(self): @@ -397,7 +397,7 @@ def size_in_weekdays(self): if self.unit in (DateUnit.DAY, DateUnit.WEEKDAY): return self.size - raise ValueError(f"Can't calculate number of weekdays in a {self.unit}.") + raise ValueError(f"Cannot calculate number of weekdays in {self.unit}.") @property def days(self): diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index 06d6803885..e3ef6b209a 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -3,7 +3,7 @@ from typing import Dict, NamedTuple, Optional, Sequence, Union from typing_extensions import TypedDict -from openfisca_core.types import Array, Entity, Period, Role, Simulation +from openfisca_core.types import Array, Period, Role, Simulation, SingleEntity import traceback @@ -16,12 +16,12 @@ class Population: simulation: Optional[Simulation] - entity: Entity + entity: SingleEntity _holders: Dict[str, holders.Holder] count: int ids: Array[str] - def __init__(self, entity: Entity) -> None: + def __init__(self, entity: SingleEntity) -> None: self.simulation = None self.entity = entity self._holders = {} 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/helpers.py b/openfisca_core/projectors/helpers.py index ce94f9773e..9c666eb6b5 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.entities.types import Entity, GroupEntity, Role +from collections.abc import Iterable, Mapping from openfisca_core import entities, projectors -from .typing import GroupPopulation, Population +from .types import GroupEntity, GroupPopulation, Role, SingleEntity, SinglePopulation def projectable(function): @@ -19,7 +17,7 @@ def projectable(function): def get_projector_from_shortcut( - population: Population | GroupPopulation, + population: SinglePopulation | GroupPopulation, shortcut: str, parent: projectors.Projector | None = None, ) -> projectors.Projector | None: @@ -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,11 +108,11 @@ def get_projector_from_shortcut( """ - entity: Entity | GroupEntity = population.entity + entity: SingleEntity | GroupEntity = population.entity if isinstance(entity, entities.Entity): populations: Mapping[ - str, Population | GroupPopulation + str, SinglePopulation | GroupPopulation ] = population.simulation.populations if shortcut not in populations.keys(): @@ -126,7 +124,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[Role] = entity.roles + role: 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..a090c4813a --- /dev/null +++ b/openfisca_core/projectors/types.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Protocol + +from openfisca_core import types as t + +# Entities + + +class SingleEntity(t.SingleEntity, Protocol): + ... + + +class GroupEntity(t.GroupEntity, Protocol): + ... + + +class Role(t.Role, Protocol): + ... + + +# Populations + + +class SinglePopulation(t.SinglePopulation, Protocol): + @property + def entity(self) -> t.SingleEntity: + ... + + @property + def simulation(self) -> Simulation: + ... + + +class GroupPopulation(t.GroupPopulation, Protocol): + @property + def entity(self) -> t.GroupEntity: + ... + + @property + def simulation(self) -> Simulation: + ... + + +# Simulations + + +class Simulation(t.Simulation, Protocol): + @property + def populations(self) -> Mapping[str, SinglePopulation | GroupPopulation]: + ... diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py deleted file mode 100644 index a6ce8e3987..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.entities.types import Entity, GroupEntity - - -class Population(Protocol): - @property - def entity(self) -> Entity: - ... - - @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/simulations/__init__.py b/openfisca_core/simulations/__init__.py index 670b922ebb..a03d846ddf 100644 --- a/openfisca_core/simulations/__init__.py +++ b/openfisca_core/simulations/__init__.py @@ -21,20 +21,17 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from openfisca_core.errors import ( # noqa: F401 - CycleError, - NaNCreationError, - SpiralError, -) +from openfisca_core.errors import CycleError, NaNCreationError, SpiralError -from .helpers import ( # noqa: F401 +from . import types +from .helpers import ( calculate_output_add, calculate_output_divide, check_type, transform_to_strict_syntax, ) -from .simulation import Simulation # noqa: F401 -from .simulation_builder import SimulationBuilder # noqa: F401 +from .simulation import Simulation +from .simulation_builder import SimulationBuilder __all__ = [ "CycleError", @@ -46,4 +43,5 @@ "calculate_output_divide", "check_type", "transform_to_strict_syntax", + "types", ] diff --git a/openfisca_core/simulations/_build_default_simulation.py b/openfisca_core/simulations/_build_default_simulation.py index f99c1d210a..f8828ff591 100644 --- a/openfisca_core/simulations/_build_default_simulation.py +++ b/openfisca_core/simulations/_build_default_simulation.py @@ -1,17 +1,23 @@ """This module contains the _BuildDefaultSimulation class.""" -from typing import Union -from typing_extensions import Self +from typing_extensions import Self, TypeAlias import numpy from .simulation import Simulation -from .typing import Entity, Population, TaxBenefitSystem +from .types import CoreEntity, GroupPopulation, TaxBenefitSystem + +Populations: TypeAlias = dict[str, GroupPopulation[CoreEntity]] class _BuildDefaultSimulation: """Build a default simulation. + Attributes: + count(int): The number of periods. + populations(Populations): The built populations. + simulation(Simulation): The built simulation. + Args: tax_benefit_system(TaxBenefitSystem): The tax-benefit system. count(int): The number of periods. @@ -47,7 +53,7 @@ class _BuildDefaultSimulation: count: int #: The built populations. - populations: dict[str, Union[Population[Entity]]] + populations: Populations #: The built simulation. simulation: Simulation @@ -61,7 +67,7 @@ def add_count(self) -> Self: """Add the number of Population to the simulation. Returns: - _BuildDefaultSimulation: The builder. + Self: The builder. Examples: >>> from openfisca_core import entities, taxbenefitsystems @@ -94,7 +100,7 @@ def add_ids(self) -> Self: """Add the populations ids to the simulation. Returns: - _BuildDefaultSimulation: The builder. + Self: The builder. Examples: >>> from openfisca_core import entities, taxbenefitsystems @@ -129,7 +135,7 @@ def add_members_entity_id(self) -> Self: Each SingleEntity has its own GroupEntity. Returns: - _BuildDefaultSimulation: The builder. + Self: The builder. Examples: >>> from openfisca_core import entities, taxbenefitsystems diff --git a/openfisca_core/simulations/_build_from_variables.py b/openfisca_core/simulations/_build_from_variables.py index 60ff6148e7..4721ebca69 100644 --- a/openfisca_core/simulations/_build_from_variables.py +++ b/openfisca_core/simulations/_build_from_variables.py @@ -2,14 +2,17 @@ from __future__ import annotations -from typing_extensions import Self +from collections.abc import Sized +from typing_extensions import Self, TypeAlias from openfisca_core import errors from ._build_default_simulation import _BuildDefaultSimulation -from ._type_guards import is_variable_dated +from ._guards import is_variable_dated from .simulation import Simulation -from .typing import Entity, Population, TaxBenefitSystem, Variables +from .types import CoreEntity, GroupPopulation, TaxBenefitSystem, Variables + +Populations: TypeAlias = dict[str, GroupPopulation[CoreEntity]] class _BuildFromVariables: @@ -67,7 +70,7 @@ class _BuildFromVariables: default_period: str | None #: The built populations. - populations: dict[str, Population[Entity]] + populations: Populations #: The built simulation. simulation: Simulation @@ -99,7 +102,7 @@ def add_dated_values(self) -> Self: """Add the dated input values to the Simulation. Returns: - _BuildFromVariables: The builder. + Self: The builder. Examples: >>> from openfisca_core import entities, periods, taxbenefitsystems, variables @@ -151,7 +154,7 @@ def add_undated_values(self) -> Self: """Add the undated input values to the Simulation. Returns: - _BuildFromVariables: The builder. + Self: The builder. Raises: SituationParsingError: If there is not a default period set. @@ -184,7 +187,7 @@ def add_undated_values(self) -> Self: >>> builder = _BuildFromVariables(tax_benefit_system, variables) >>> builder.add_undated_values() Traceback (most recent call last): - openfisca_core.errors.situation_parsing_error.SituationParsingError + openfisca_core.errors.situation_parsing_error.SituationParsingEr... >>> builder.default_period = period >>> builder.add_undated_values() <..._BuildFromVariables object at ...> @@ -218,7 +221,7 @@ def add_undated_values(self) -> Self: def _person_count(params: Variables) -> int: try: - first_value = next(iter(params.values())) + first_value: object = next(iter(params.values())) if isinstance(first_value, dict): first_value = next(iter(first_value.values())) @@ -226,7 +229,10 @@ def _person_count(params: Variables) -> int: if isinstance(first_value, str): return 1 - return len(first_value) + if isinstance(first_value, Sized): + return len(first_value) + + raise NotImplementedError except Exception: return 1 diff --git a/openfisca_core/simulations/_type_guards.py b/openfisca_core/simulations/_guards.py similarity index 99% rename from openfisca_core/simulations/_type_guards.py rename to openfisca_core/simulations/_guards.py index c34361041a..79e0c85840 100644 --- a/openfisca_core/simulations/_type_guards.py +++ b/openfisca_core/simulations/_guards.py @@ -5,7 +5,7 @@ from typing import Iterable from typing_extensions import TypeGuard -from .typing import ( +from .types import ( Axes, DatedVariable, FullySpecifiedEntities, diff --git a/openfisca_core/simulations/_rules.py b/openfisca_core/simulations/_rules.py new file mode 100644 index 0000000000..d602c5c7a3 --- /dev/null +++ b/openfisca_core/simulations/_rules.py @@ -0,0 +1,119 @@ +"""Rules for simulations, aka business invariants.""" + +from typing import TypedDict, Union +from typing_extensions import TypeAlias + +from openfisca_core import commons, periods + +from .types import Failure, Period, Success, Variable + +#: Type alias for an either monad. +Either: TypeAlias = Union[Failure[str], Success["_State"]] + + +class _State(TypedDict): + """State of the rule-checking.""" + + #: The variable to check. + variable: Variable + + #: The period to check. + period: Period + + +def _check_periods_compatibility_1(state: _State) -> Either: + """When definition period is month/day and period is week. + + Examples: + >>> from openfisca_core import entities, periods, variables + + >>> entity = entities.SingleEntity("", "", "", "") + + >>> class Variable(variables.Variable): + ... definition_period = periods.WEEK + ... entity = entity + ... value_type = int + + >>> variable = Variable() + >>> period = periods.period("2020-W01") + >>> state = {"variable": variable, "period": period} + + >>> _check_periods_compatibility_1(state) + Success(_value={'variable': ..., 'period': ...}) + + >>> variable.definition_period = periods.MONTH + >>> _check_periods_compatibility_1(state) + Failure(_value="Unable to compute variable 'Variable' for period 2...") + + Args: + state(_State): The state of the rule-checking. + + Returns: + Either: The result of the rule-checking. + + """ + + variable = state["variable"] + period = state["period"] + + if ( + variable.definition_period in (periods.MONTH, periods.DAY) + and period.unit == periods.WEEK + ): + return commons.either.fail( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year'." + ) + + return commons.either.succeed(state) + + +def _check_periods_compatibility_2(state: _State) -> Either: + """When definition period is week/weekday and period is month. + + Examples: + >>> from openfisca_core import entities, periods, variables + + >>> entity = entities.SingleEntity("", "", "", "") + + >>> class Variable(variables.Variable): + ... definition_period = periods.YEAR + ... entity = entity + ... value_type = int + + >>> variable = Variable() + >>> period = periods.period("2020-01") + >>> state = {"variable": variable, "period": period} + + >>> _check_periods_compatibility_2(state) + Success(_value={'variable': ..., 'period': ...}) + + >>> variable.definition_period = periods.WEEKDAY + >>> _check_periods_compatibility_2(state) + Failure(_value="Unable to compute variable 'Variable' for period 2...") + + Args: + state(_State): The state of the rule-checking. + + Returns: + Either: The result of the rule-checking. + + """ + + variable = state["variable"] + period = state["period"] + + if ( + variable.definition_period in (periods.WEEK, periods.WEEKDAY) + and period.unit == periods.MONTH + ): + return commons.either.fail( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year' or 'period.first_week'." + ) + + return commons.either.succeed(state) diff --git a/openfisca_core/simulations/helpers.py b/openfisca_core/simulations/helpers.py index d5984d88b6..edc3fa1ad3 100644 --- a/openfisca_core/simulations/helpers.py +++ b/openfisca_core/simulations/helpers.py @@ -2,7 +2,7 @@ from openfisca_core import errors -from .typing import ParamsWithoutAxes +from .types import ParamsWithoutAxes def calculate_output_add(simulation, variable_name: str, period): @@ -58,7 +58,7 @@ def check_unexpected_entities( >>> check_unexpected_entities(params, entities) Traceback (most recent call last): - openfisca_core.errors.situation_parsing_error.SituationParsingError + openfisca_core.errors.situation_parsing_error.SituationParsingError... """ diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 93becda960..5c3cc0efe0 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,8 +1,6 @@ from __future__ import annotations -from typing import Dict, Mapping, NamedTuple, Optional, Set - -from openfisca_core.types import Population, TaxBenefitSystem, Variable +from typing import NamedTuple, Optional, Set import tempfile import warnings @@ -12,6 +10,8 @@ from openfisca_core import commons, errors, indexed_enums, periods, tracers from openfisca_core import warnings as core_warnings +from .types import SinglePopulation, TaxBenefitSystem, Variable + class Simulation: """ @@ -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: dict[str, SinglePopulation], ): """ This constructor is reserved for internal use; see :any:`SimulationBuilder`, @@ -175,6 +175,29 @@ def calculate_add(self, variable_name: str, period): if period is not None and not isinstance(period, periods.Period): period = periods.period(period) + # Rule out incompatible periods. + if ( + variable.definition_period in (periods.MONTH, periods.DAY) + and period.unit == periods.WEEK + ): + raise ValueError( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year'." + ) + + if ( + variable.definition_period in (periods.WEEK, periods.WEEKDAY) + and period.unit == periods.MONTH + ): + raise ValueError( + f"Unable to compute variable '{variable.name}' for period " + f"{period}, as {period} and {variable.definition_period} are " + "incompatible periods. You can, however, change the requested " + "period to 'period.this_year' or 'period.first_week'." + ) + # Check that the requested period matches definition_period if periods.unit_weight(variable.definition_period) > periods.unit_weight( period.unit @@ -186,9 +209,7 @@ def calculate_add(self, variable_name: str, period): f"DIVIDE option to get an estimate of {variable.name}." ) - if variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar - ): + if variable.definition_period not in (periods.ISOFORMAT + periods.ISOCALENDAR): raise ValueError( f"Unable to ADD constant variable '{variable.name}' over " f"the period {period}: eternal variables can't be summed " @@ -225,9 +246,7 @@ def calculate_divide(self, variable_name: str, period): f"ADD option to get an estimate of {variable.name}." ) - if variable.definition_period not in ( - periods.DateUnit.isoformat + periods.DateUnit.isocalendar - ): + if variable.definition_period not in (periods.ISOFORMAT + periods.ISOCALENDAR): raise ValueError( f"Unable to DIVIDE constant variable '{variable.name}' over " f"the period {period}: eternal variables can't be divided " @@ -235,8 +254,7 @@ def calculate_divide(self, variable_name: str, period): ) if ( - period.unit - not in (periods.DateUnit.isoformat + periods.DateUnit.isocalendar) + period.unit not in (periods.ISOFORMAT + periods.ISOCALENDAR) or period.size != 1 ): raise ValueError( @@ -463,23 +481,23 @@ def delete_arrays(self, variable, period=None): Example: - >>> from openfisca_country_template import CountryTaxBenefitSystem - >>> simulation = Simulation(CountryTaxBenefitSystem()) - >>> simulation.set_input('age', '2018-04', [12, 14]) - >>> simulation.set_input('age', '2018-05', [13, 14]) - >>> simulation.get_array('age', '2018-05') - array([13, 14], dtype=int32) - >>> simulation.delete_arrays('age', '2018-05') - >>> simulation.get_array('age', '2018-04') - array([12, 14], dtype=int32) - >>> simulation.get_array('age', '2018-05') is None - True - >>> simulation.set_input('age', '2018-05', [13, 14]) - >>> simulation.delete_arrays('age') - >>> simulation.get_array('age', '2018-04') is None - True - >>> simulation.get_array('age', '2018-05') is None - True + # >>> from openfisca_country_template import CountryTaxBenefitSystem + # >>> simulation = Simulation(CountryTaxBenefitSystem()) + # >>> simulation.set_input('age', '2018-04', [12, 14]) + # >>> simulation.set_input('age', '2018-05', [13, 14]) + # >>> simulation.get_array('age', '2018-05') + # array([13, 14], dtype=int32) + # >>> simulation.delete_arrays('age', '2018-05') + # >>> simulation.get_array('age', '2018-04') + # array([12, 14], dtype=int32) + # >>> simulation.get_array('age', '2018-05') is None + # True + # >>> simulation.set_input('age', '2018-05', [13, 14]) + # >>> simulation.delete_arrays('age') + # >>> simulation.get_array('age', '2018-04') is None + # True + # >>> simulation.get_array('age', '2018-05') is None + # True """ self.get_holder(variable).delete_arrays(period) @@ -491,12 +509,12 @@ def get_known_periods(self, variable): Example: - >>> from openfisca_country_template import CountryTaxBenefitSystem - >>> simulation = Simulation(CountryTaxBenefitSystem()) - >>> simulation.set_input('age', '2018-04', [12, 14]) - >>> simulation.set_input('age', '2018-05', [13, 14]) - >>> simulation.get_known_periods('age') - [Period((u'month', Instant((2018, 5, 1)), 1)), Period((u'month', Instant((2018, 4, 1)), 1))] + # >>> from openfisca_country_template import CountryTaxBenefitSystem + # >>> simulation = Simulation(CountryTaxBenefitSystem()) + # >>> simulation.set_input('age', '2018-04', [12, 14]) + # >>> simulation.set_input('age', '2018-05', [13, 14]) + # >>> simulation.get_known_periods('age') + # [Period((u'month', Instant((2018, 5, 1)), 1)), Period((u'month', Instant((2018, 4, 1)), 1))] """ return self.get_holder(variable).get_known_periods() @@ -509,11 +527,11 @@ def set_input(self, variable_name: str, period, value): :param period: the period for which the value is setted Example: - >>> from openfisca_country_template import CountryTaxBenefitSystem - >>> simulation = Simulation(CountryTaxBenefitSystem()) - >>> simulation.set_input('age', '2018-04', [12, 14]) - >>> simulation.get_array('age', '2018-04') - array([12, 14], dtype=int32) + # >>> from openfisca_country_template import CountryTaxBenefitSystem + # >>> simulation = Simulation(CountryTaxBenefitSystem()) + # >>> simulation.set_input('age', '2018-04', [12, 14]) + # >>> simulation.get_array('age', '2018-04') + # array([12, 14], dtype=int32) If a ``set_input`` property has been set for the variable, this method may accept inputs for periods not matching the ``definition_period`` of the variable. To read more about this, check the `documentation `_. """ @@ -531,7 +549,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 +561,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 +576,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/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index c42d0e4f22..7b5860082d 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -14,25 +14,24 @@ from . import helpers from ._build_default_simulation import _BuildDefaultSimulation from ._build_from_variables import _BuildFromVariables -from ._type_guards import ( +from ._guards import ( are_entities_fully_specified, are_entities_short_form, are_entities_specified, has_axes, ) from .simulation import Simulation -from .typing import ( +from .types import ( Axis, - Entity, FullySpecifiedEntities, GroupEntities, GroupEntity, ImplicitGroupEntities, Params, ParamsWithoutAxes, - Population, Role, SingleEntity, + SinglePopulation, TaxBenefitSystem, Variables, ) @@ -748,7 +747,7 @@ def expand_axes(self) -> None: if len(self.axes) == 1 and len(self.axes[0]): parallel_axes = self.axes[0] first_axis = parallel_axes[0] - axis_count: int = first_axis["count"] + axis_count = first_axis["count"] axis_entity = self.get_variable_entity(first_axis["name"]) axis_entity_step_size = self.entity_counts[axis_entity.plural] # Distribute values along axes @@ -803,10 +802,10 @@ def expand_axes(self) -> None: ) self.input_buffer[axis_name][str(axis_period)] = array - def get_variable_entity(self, variable_name: str) -> Entity: + def get_variable_entity(self, variable_name: str) -> SingleEntity: return self.variable_entities[variable_name] - def register_variable(self, variable_name: str, entity: Entity) -> None: + def register_variable(self, variable_name: str, entity: SingleEntity) -> None: self.variable_entities[variable_name] = entity def register_variables(self, simulation: Simulation) -> None: @@ -814,6 +813,6 @@ def register_variables(self, simulation: Simulation) -> None: variables: Iterable[str] = tax_benefit_system.variables.keys() for name in variables: - population: Population = simulation.get_variable_population(name) - entity: Entity = population.entity + population: SinglePopulation = simulation.get_variable_population(name) + entity: SingleEntity = population.entity self.register_variable(name, entity) diff --git a/openfisca_core/simulations/tests/__init__.py b/openfisca_core/simulations/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/simulations/tests/test_rules.py b/openfisca_core/simulations/tests/test_rules.py new file mode 100644 index 0000000000..e2c0946508 --- /dev/null +++ b/openfisca_core/simulations/tests/test_rules.py @@ -0,0 +1,163 @@ +from openfisca_core.simulations.types import Variable + +import pytest + +from openfisca_core import commons, periods +from openfisca_core.simulations import _rules + + +class TestVariable(Variable): + def __init__(self, name, definition_period): + self.name = name + self.definition_period = definition_period + + +def derive_calculate_add(state): + either = ( + commons.either(state) + .then(_rules._check_periods_compatibility_1) + .then(_rules._check_periods_compatibility_2) + ) + + if either.is_success: + return either.unwrap() + + raise ValueError(either.unwrap()) + + +@pytest.mark.parametrize( + "period_unit, period_str, expected", + [ + (periods.YEAR, "2020", True), + (periods.YEAR, "2020-01", True), + (periods.YEAR, "2020-01-01", True), + (periods.YEAR, "2020-W01", True), + (periods.YEAR, "2020-W01-1", True), + (periods.MONTH, "2020", True), + (periods.MONTH, "2020-01", True), + (periods.MONTH, "2020-01-01", True), + (periods.MONTH, "2020-W01", False), + (periods.MONTH, "2020-W01-1", True), + (periods.DAY, "2020", True), + (periods.DAY, "2020-01", True), + (periods.DAY, "2020-01-01", True), + (periods.DAY, "2020-W01", False), + (periods.DAY, "2020-W01-1", True), + (periods.WEEK, "2020", True), + (periods.WEEK, "2020-01", True), + (periods.WEEK, "2020-01-01", True), + (periods.WEEK, "2020-W01", True), + (periods.WEEK, "2020-W01-1", True), + (periods.WEEKDAY, "2020", True), + (periods.WEEKDAY, "2020-01", True), + (periods.WEEKDAY, "2020-01-01", True), + (periods.WEEKDAY, "2020-W01", True), + (periods.WEEKDAY, "2020-W01-1", True), + ], +) +def test_are_periods_compatible_1(period_unit, period_str, expected): + variable = TestVariable("variable", period_unit) + period = periods.period(period_str) + either = _rules._check_periods_compatibility_1( + {"variable": variable, "period": period} + ) + assert either.is_success is expected + + +# @pytest.mark.parametrize( +# "period_unit, period_str, expected", +# [ +# (periods.YEAR, "2020", True), +# (periods.YEAR, "2020-01", True), +# (periods.YEAR, "2020-01-01", True), +# (periods.YEAR, "2020-W01", True), +# (periods.YEAR, "2020-W01-1", True), +# (periods.MONTH, "2020", True), +# (periods.MONTH, "2020-01", True), +# (periods.MONTH, "2020-01-01", True), +# (periods.MONTH, "2020-W01", True), +# (periods.MONTH, "2020-W01-1", True), +# (periods.DAY, "2020", True), +# (periods.DAY, "2020-01", True), +# (periods.DAY, "2020-01-01", True), +# (periods.DAY, "2020-W01", True), +# (periods.DAY, "2020-W01-1", True), +# (periods.WEEK, "2020", True), +# (periods.WEEK, "2020-01", False), +# (periods.WEEK, "2020-01-01", True), +# (periods.WEEK, "2020-W01", True), +# (periods.WEEK, "2020-W01-1", True), +# (periods.WEEKDAY, "2020", True), +# (periods.WEEKDAY, "2020-01", False), +# (periods.WEEKDAY, "2020-01-01", True), +# (periods.WEEKDAY, "2020-W01", True), +# (periods.WEEKDAY, "2020-W01-1", True), +# ], +# ) +# def test_are_periods_compatible_2(period_unit, period_str, expected): +# variable = TestVariable("variable", period_unit) +# period = periods.period(period_str) +# either = _rules._check_periods_compatibility_2({variable: variable, period: period}) +# assert either.is_success is expected +# +# +# @pytest.mark.parametrize( +# "period_unit, period_str", +# [ +# (periods.YEAR, "2020"), +# (periods.YEAR, "2020-01"), +# (periods.YEAR, "2020-01-01"), +# (periods.YEAR, "2020-W01"), +# (periods.YEAR, "2020-W01-1"), +# (periods.MONTH, "2020"), +# (periods.MONTH, "2020-01"), +# (periods.MONTH, "2020-01-01"), +# (periods.MONTH, "2020-W01-1"), +# (periods.DAY, "2020"), +# (periods.DAY, "2020-01"), +# (periods.DAY, "2020-01-01"), +# (periods.DAY, "2020-W01-1"), +# (periods.WEEK, "2020"), +# (periods.WEEK, "2020-01-01"), +# (periods.WEEK, "2020-W01"), +# (periods.WEEK, "2020-W01-1"), +# (periods.WEEKDAY, "2020"), +# (periods.WEEKDAY, "2020-01-01"), +# (periods.WEEKDAY, "2020-W01"), +# (periods.WEEKDAY, "2020-W01-1"), +# ], +# ) +# def test_derive_calculate_add(period_unit, period_str): +# variable = TestVariable("variable", period_unit) +# period = periods.period(period_str) +# assert derive_calculate_add({variable: variable, period: period}) +# +# +# @pytest.mark.parametrize( +# "period_unit, period_str", +# [ +# (periods.MONTH, "2020-W01"), +# (periods.DAY, "2020-W01"), +# ], +# ) +# def test_derive_calculate_add_with_invalid_period_1(period_unit, period_str): +# variable = TestVariable("variable", period_unit) +# period = periods.period(period_str) +# with pytest.raises(ValueError) as error: +# derive_calculate_add({variable: variable, period: period}) +# assert "period.first_week" not in str(error.value) +# +# +# @pytest.mark.parametrize( +# "period_unit, period_str", +# [ +# (periods.WEEK, "2020-01"), +# (periods.WEEKDAY, "2020-01"), +# ], +# ) +# def test_derive_calculate_add_with_invalid_period_2(period_unit, period_str): +# variable = TestVariable("variable", period_unit) +# period = periods.period(period_str) +# with pytest.raises(ValueError) as error: +# derive_calculate_add({variable: variable, period: period}) +# assert "period.first_week" in str(error.value) diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/types.py similarity index 60% rename from openfisca_core/simulations/typing.py rename to openfisca_core/simulations/types.py index 8603d0d811..b573450391 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/types.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Iterable, Sequence -from numpy.typing import NDArray as Array from typing import Protocol, TypeVar, TypedDict, Union from typing_extensions import NotRequired, Required, TypeAlias @@ -17,67 +16,45 @@ from numpy import int32 as Int from numpy import str_ as String -#: Generic type variables. -E = TypeVar("E") +from openfisca_core import types as t + +# Generic type variables. +D = TypeVar("D") +E = TypeVar("E", covariant=True) G = TypeVar("G", covariant=True) T = TypeVar("T", Bool, Date, Enum, Float, Int, String, covariant=True) U = TypeVar("U", bool, datetime.date, float, str) V = TypeVar("V", covariant=True) +#: Type variable representing an error. +F = TypeVar("F", covariant=True) -#: Type alias for a simulation dictionary defining the roles. -Roles: TypeAlias = dict[str, Union[str, Iterable[str]]] - -#: Type alias for a simulation dictionary with undated variables. -UndatedVariable: TypeAlias = dict[str, object] - -#: Type alias for a simulation dictionary with dated variables. -DatedVariable: TypeAlias = dict[str, UndatedVariable] - -#: Type alias for a simulation dictionary with abbreviated entities. -Variables: TypeAlias = dict[str, Union[UndatedVariable, DatedVariable]] - -#: Type alias for a simulation with fully specified single entities. -SingleEntities: TypeAlias = dict[str, dict[str, Variables]] +#: Type variable representing a value. +A = TypeVar("A", covariant=True) -#: Type alias for a simulation dictionary with implicit group entities. -ImplicitGroupEntities: TypeAlias = dict[str, Union[Roles, Variables]] +#: Type alias for numpy arrays values. +Item: TypeAlias = Union[Bool, Date, Enum, Float, Int, String] -#: Type alias for a simulation dictionary with explicit group entities. -GroupEntities: TypeAlias = dict[str, ImplicitGroupEntities] -#: Type alias for a simulation dictionary with fully specified entities. -FullySpecifiedEntities: TypeAlias = Union[SingleEntities, GroupEntities] +# Commons -#: Type alias for a simulation dictionary with axes parameters. -Axes: TypeAlias = dict[str, Iterable[Iterable["Axis"]]] -#: Type alias for a simulation dictionary without axes parameters. -ParamsWithoutAxes: TypeAlias = Union[ - Variables, ImplicitGroupEntities, FullySpecifiedEntities -] +class Failure(t.Failure[F], Protocol[F]): + ... -#: Type alias for a simulation dictionary with axes parameters. -ParamsWithAxes: TypeAlias = Union[Axes, ParamsWithoutAxes] -#: Type alias for a simulation dictionary with all the possible scenarios. -Params: TypeAlias = ParamsWithAxes +class Success(t.Success[A], Protocol[A]): + ... -class Axis(TypedDict, total=False): - """Interface representing an axis of a simulation.""" +# Entities - count: Required[int] - index: NotRequired[int] - max: Required[float] - min: Required[float] - name: Required[str] - period: NotRequired[str | int] +#: Type alias for a simulation dictionary defining the roles. +Roles: TypeAlias = dict[str, Union[str, Iterable[str]]] -class Entity(Protocol): - """Interface representing an entity of a simulation.""" +class CoreEntity(t.CoreEntity, Protocol): key: str plural: str | None @@ -86,114 +63,165 @@ def get_variable( __variable_name: str, __check_existence: bool = ..., ) -> Variable[T] | None: - """Get a variable.""" - + ... -class SingleEntity(Entity, Protocol): - """Interface representing a single entity of a simulation.""" +class SingleEntity(t.SingleEntity, Protocol): + ... -class GroupEntity(Entity, Protocol): - """Interface representing a group entity of a simulation.""" +class GroupEntity(t.GroupEntity, Protocol): @property @abstractmethod def flattened_roles(self) -> Iterable[Role[G]]: - """Get the flattened roles of the GroupEntity.""" + ... + +class Role(t.Role, Protocol[G]): + ... -class Holder(Protocol[V]): - """Interface representing a holder of a simulation's computed values.""" +# Holders + + +class Holder(t.Holder, Protocol[V]): @property @abstractmethod def variable(self) -> Variable[T]: - """Get the Variable of the Holder.""" + ... - def get_array(self, __period: str) -> Array[T] | None: - """Get the values of the Variable for a given Period.""" + def get_array(self, __period: str) -> t.Array[T] | None: + ... def set_input( self, __period: Period, - __array: Array[T] | Sequence[U], - ) -> Array[T] | None: - """Set values for a Variable for a given Period.""" + __array: t.Array[T] | Sequence[U], + ) -> t.Array[T] | None: + ... -class Period(Protocol): - """Interface representing a period of a simulation.""" +# Periods -class Population(Protocol[E]): - """Interface representing a data vector of an Entity.""" +class Period(t.Period, Protocol): + ... - count: int - entity: E - ids: Array[String] - def get_holder(self, __variable_name: str) -> Holder[V]: - """Get the holder of a Variable.""" +# Populations -class SinglePopulation(Population[E], Protocol): - """Interface representing a data vector of a SingleEntity.""" +class CorePopulation(t.CorePopulation, Protocol[D]): + entity: D + + def get_holder(self, __variable_name: str) -> Holder[V]: + ... + +class SinglePopulation(t.SinglePopulation, Protocol[E]): + ... -class GroupPopulation(Population[E], Protocol): - """Interface representing a data vector of a GroupEntity.""" - members_entity_id: Array[String] +class GroupPopulation(t.GroupPopulation, Protocol[E]): + members_entity_id: t.Array[String] def nb_persons(self, __role: Role[G] | None = ...) -> int: - """Get the number of persons for a given Role.""" + ... -class Role(Protocol[G]): - """Interface representing a role of the group entities of a simulation.""" +# Simulations -class TaxBenefitSystem(Protocol): - """Interface representing a tax-benefit system.""" +#: Type alias for a simulation dictionary with undated variables. +UndatedVariable: TypeAlias = dict[str, object] + +#: Type alias for a simulation dictionary with dated variables. +DatedVariable: TypeAlias = dict[str, UndatedVariable] + +#: Type alias for a simulation dictionary with abbreviated entities. +Variables: TypeAlias = dict[str, Union[UndatedVariable, DatedVariable]] + +#: Type alias for a simulation with fully specified single entities. +SingleEntities: TypeAlias = dict[str, dict[str, Variables]] + +#: Type alias for a simulation dictionary with implicit group entities. +ImplicitGroupEntities: TypeAlias = dict[str, Union[Roles, Variables]] + +#: Type alias for a simulation dictionary with explicit group entities. +GroupEntities: TypeAlias = dict[str, ImplicitGroupEntities] + +#: Type alias for a simulation dictionary with fully specified entities. +FullySpecifiedEntities: TypeAlias = Union[SingleEntities, GroupEntities] + +#: Type alias for a simulation dictionary with axes parameters. +Axes: TypeAlias = dict[str, Iterable[Iterable["Axis"]]] +#: Type alias for a simulation dictionary without axes parameters. +ParamsWithoutAxes: TypeAlias = Union[ + Variables, ImplicitGroupEntities, FullySpecifiedEntities +] + +#: Type alias for a simulation dictionary with axes parameters. +ParamsWithAxes: TypeAlias = Union[Axes, ParamsWithoutAxes] + +#: Type alias for a simulation dictionary with all the possible scenarios. +Params: TypeAlias = ParamsWithAxes + + +class Axis(TypedDict, total=False): + count: Required[int] + index: NotRequired[int] + max: Required[float] + min: Required[float] + name: Required[str] + period: NotRequired[str | int] + + +# Tax-Benefit systems + + +class TaxBenefitSystem(t.TaxBenefitSystem, Protocol): @property @abstractmethod def person_entity(self) -> SingleEntity: - """Get the person entity of the tax-benefit system.""" + ... @person_entity.setter @abstractmethod def person_entity(self, person_entity: SingleEntity) -> None: - """Set the person entity of the tax-benefit system.""" + ... @property @abstractmethod def variables(self) -> dict[str, V]: - """Get the variables of the tax-benefit system.""" + ... def entities_by_singular(self) -> dict[str, E]: - """Get the singular form of the entities' keys.""" + ... def entities_plural(self) -> Iterable[str]: - """Get the plural form of the entities' keys.""" + ... def get_variable( self, __variable_name: str, __check_existence: bool = ..., ) -> V | None: - """Get a variable.""" + ... def instantiate_entities( self, - ) -> dict[str, Population[E]]: - """Instantiate the populations of each Entity.""" + ) -> dict[str, GroupPopulation[E]]: + ... + +# Variables -class Variable(Protocol[T]): - """Interface representing a variable of a tax-benefit system.""" +class Variable(t.Variable, Protocol[T]): + definition_period: str end: str + name: str - def default_array(self, __array_size: int) -> Array[T]: - """Fill an array with the default value of the Variable.""" + def default_array(self, __array_size: int) -> t.Array[T]: + ... diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types.py similarity index 56% rename from openfisca_core/types/_domain.py rename to openfisca_core/types.py index d324f1b2cf..95f3109b52 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types.py @@ -1,134 +1,172 @@ from __future__ import annotations import typing_extensions -from typing import Any, Optional -from typing_extensions import Protocol +from collections.abc import Sequence +from numpy.typing import NDArray +from typing import Any, TypeVar +from typing_extensions import Protocol, TypeAlias import abc import numpy +N = TypeVar("N", bound=numpy.generic, covariant=True) -class Entity(Protocol): - """Entity protocol.""" +#: Type representing an numpy array. +Array: TypeAlias = NDArray[N] +L = TypeVar("L") + +#: Type representing an array-like object. +ArrayLike: TypeAlias = Sequence[L] + +#: Type variable representing an error. +E = TypeVar("E", covariant=True) + +#: Type variable representing a value. +A = TypeVar("A", covariant=True) + + +# Commons + + +class Failure(Protocol[E]): + ... + + +class Success(Protocol[A]): + ... + + +# Entities + + +class CoreEntity(Protocol): key: Any plural: Any @abc.abstractmethod def check_role_validity(self, role: Any) -> None: - """Abstract method.""" + ... @abc.abstractmethod def check_variable_defined_for_entity(self, variable_name: Any) -> None: - """Abstract method.""" + ... @abc.abstractmethod def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Optional[Any]: - """Abstract method.""" + ) -> Any | None: + ... -class Formula(Protocol): - """Formula protocol.""" +class SingleEntity(CoreEntity, Protocol): + ... - @abc.abstractmethod - def __call__( - self, - population: Population, - instant: Instant, - params: Params, - ) -> numpy.ndarray: - """Abstract method.""" +class GroupEntity(CoreEntity, Protocol): + ... -class Holder(Protocol): - """Holder protocol.""" +class Role(Protocol): + entity: Any + max: int | None + subroles: Any + + @property + def key(self) -> str: + ... + + +# Holders + + +class Holder(Protocol): @abc.abstractmethod def clone(self, population: Any) -> Holder: - """Abstract method.""" + ... @abc.abstractmethod def get_memory_usage(self) -> Any: - """Abstract method.""" + ... -class Instant(Protocol): - """Instant protocol.""" +# Parameters @typing_extensions.runtime_checkable class ParameterNodeAtInstant(Protocol): - """ParameterNodeAtInstant protocol.""" + ... -class Params(Protocol): - """Params protocol.""" +# Periods - @abc.abstractmethod - def __call__(self, instant: Instant) -> ParameterNodeAtInstant: - """Abstract method.""" + +class Instant(Protocol): + ... @typing_extensions.runtime_checkable class Period(Protocol): - """Period protocol.""" - @property @abc.abstractmethod def start(self) -> Any: - """Abstract method.""" + ... @property @abc.abstractmethod def unit(self) -> Any: - """Abstract method.""" + ... + +# Populations -class Population(Protocol): - """Population protocol.""" +class CorePopulation(Protocol): + count: int entity: Any + ids: Array[numpy.str_] @abc.abstractmethod def get_holder(self, variable_name: Any) -> Any: - """Abstract method.""" + ... -class Role(Protocol): - """Role protocol.""" +class SinglePopulation(CorePopulation, Protocol): + ... - entity: Any - subroles: Any +class GroupPopulation(CorePopulation, Protocol): + ... + + +# Simulations -class Simulation(Protocol): - """Simulation protocol.""" +class Simulation(Protocol): @abc.abstractmethod def calculate(self, variable_name: Any, period: Any) -> Any: - """Abstract method.""" + ... @abc.abstractmethod def calculate_add(self, variable_name: Any, period: Any) -> Any: - """Abstract method.""" + ... @abc.abstractmethod def calculate_divide(self, variable_name: Any, period: Any) -> Any: - """Abstract method.""" + ... @abc.abstractmethod - def get_population(self, plural: Optional[Any]) -> Any: - """Abstract method.""" + def get_population(self, plural: Any | None) -> Any: + ... -class TaxBenefitSystem(Protocol): - """TaxBenefitSystem protocol.""" +# Tax-Benefit systems + +class TaxBenefitSystem(Protocol): person_entity: Any @abc.abstractmethod @@ -136,11 +174,29 @@ def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Optional[Any]: + ) -> Any | None: """Abstract method.""" -class Variable(Protocol): - """Variable protocol.""" +# Variables + +class Variable(Protocol): entity: Any + + +class Formula(Protocol): + @abc.abstractmethod + def __call__( + self, + population: CorePopulation, + instant: Instant, + params: Params, + ) -> Array[Any]: + ... + + +class Params(Protocol): + @abc.abstractmethod + def __call__(self, instant: Instant) -> ParameterNodeAtInstant: + ... diff --git a/openfisca_core/types/__init__.py b/openfisca_core/types/__init__.py deleted file mode 100644 index eb403c46c9..0000000000 --- a/openfisca_core/types/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Data types and protocols used by OpenFisca Core. - -The type definitions included in this sub-package are intended for -contributors, to help them better understand and document contracts -and expected behaviours. - -Official Public API: - * :attr:`.Array` - * ``ArrayLike`` - * :attr:`.Cache` - * :attr:`.Entity` - * :attr:`.Formula` - * :attr:`.Holder` - * :attr:`.Instant` - * :attr:`.ParameterNodeAtInstant` - * :attr:`.Params` - * :attr:`.Period` - * :attr:`.Population` - * :attr:`.Role`, - * :attr:`.Simulation`, - * :attr:`.TaxBenefitSystem` - * :attr:`.Variable` - -Note: - How imports are being used today:: - - from openfisca_core.types import * # Bad - from openfisca_core.types.data_types.arrays import ArrayLike # Bad - - The previous examples provoke cyclic dependency problems, that prevents us - from modularizing the different components of the library, so as to make - them easier to test and to maintain. - - How could them be used after the next major release:: - - from openfisca_core.types import ArrayLike - - ArrayLike # Good: import types as publicly exposed - - .. seealso:: `PEP8#Imports`_ and `OpenFisca's Styleguide`_. - - .. _PEP8#Imports: - https://www.python.org/dev/peps/pep-0008/#imports - - .. _OpenFisca's Styleguide: - https://github.com/openfisca/openfisca-core/blob/master/STYLEGUIDE.md - -""" - -# Official Public API - -from ._data import Array, ArrayLike # noqa: F401 -from ._domain import ( # noqa: F401 - Entity, - Formula, - Holder, - Instant, - ParameterNodeAtInstant, - Params, - Period, - Population, - Role, - Simulation, - TaxBenefitSystem, - Variable, -) - -__all__ = [ - "Array", - "ArrayLike", - "Entity", - "Formula", - "Holder", - "Instant", - "ParameterNodeAtInstant", - "Params", - "Period", - "Population", - "Role", - "Simulation", - "TaxBenefitSystem", - "Variable", -] diff --git a/openfisca_core/types/_data.py b/openfisca_core/types/_data.py deleted file mode 100644 index 928e1b9174..0000000000 --- a/openfisca_core/types/_data.py +++ /dev/null @@ -1,54 +0,0 @@ -# from typing import Sequence, TypeVar, Union -# from nptyping import types, NDArray as Array -from numpy.typing import NDArray as Array # noqa: F401 -from typing import Sequence, TypeVar - -# import numpy - -# NumpyT = TypeVar("NumpyT", numpy.bytes_, numpy.number, numpy.object_, numpy.str_) -T = TypeVar("T", bool, bytes, float, int, object, str) - -# types._ndarray_meta._Type = Union[type, numpy.dtype, TypeVar] - -# ArrayLike = Union[Array[T], Sequence[T]] -ArrayLike = Sequence[T] -""":obj:`typing.Generic`: Type of any castable to :class:`numpy.ndarray`. - -These include any :obj:`numpy.ndarray` and sequences (like -:obj:`list`, :obj:`tuple`, and so on). - -Examples: - >>> ArrayLike[float] - typing.Union[numpy.ndarray, typing.Sequence[float]] - - >>> ArrayLike[str] - typing.Union[numpy.ndarray, typing.Sequence[str]] - -Note: - It is possible since numpy version 1.21 to specify the type of an - array, thanks to `numpy.typing.NDArray`_:: - - from numpy.typing import NDArray - NDArray[numpy.float64] - - `mypy`_ provides `duck type compatibility`_, so an :obj:`int` is - considered to be valid whenever a :obj:`float` is expected. - -Todo: - * Refactor once numpy version >= 1.21 is used. - -.. versionadded:: 35.5.0 - -.. versionchanged:: 35.6.0 - Moved to :mod:`.types` - -.. _mypy: - https://mypy.readthedocs.io/en/stable/ - -.. _duck type compatibility: - https://mypy.readthedocs.io/en/stable/duck_type_compatibility.html - -.. _numpy.typing.NDArray: - https://numpy.org/doc/stable/reference/typing.html#numpy.typing.NDArray - -""" diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 324104f7c0..d1805bab96 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -1,5 +1,5 @@ ## Lint the codebase. -lint: check-syntax-errors check-style lint-doc +lint: check-syntax-errors check-style lint-doc check-types @$(call print_pass,$@:) ## Compile python files to check for syntax errors. @@ -20,8 +20,12 @@ check-style: $(shell git ls-files "*.py") lint-doc: \ lint-doc-commons \ lint-doc-entities \ - lint-doc-types \ ; + @flake8 --select=D101,D102,D103,DAR \ + openfisca_core/simulations/_build_default_simulation.py \ + openfisca_core/simulations/_build_from_variables.py \ + openfisca_core/simulations/_guards.py \ + openfisca_core/simulations/_rules.py ## Run linters to check for syntax and style errors in the doc. lint-doc-%: @@ -29,7 +33,7 @@ lint-doc-%: @## @## They can be integrated into setup.cfg once all checks pass. @## The reason they're here is because otherwise we wouldn't be - @## able to integrate documentation improvements progresively. + @## able to integrate documentation improvements progressively. @## @$(call print_help,$(subst $*,%,$@:)) @flake8 --select=D101,D102,D103,DAR openfisca_core/$* @@ -39,7 +43,17 @@ lint-doc-%: ## Run static type checkers for type errors. check-types: @$(call print_help,$@:) - @mypy openfisca_core/entities openfisca_core/projectors + @mypy \ + openfisca_core/commons \ + openfisca_core/entities \ + openfisca_core/projectors \ + openfisca_core/simulations/_build_default_simulation.py \ + openfisca_core/simulations/_build_from_variables.py \ + openfisca_core/simulations/_guards.py \ + openfisca_core/simulations/_rules.py \ + openfisca_core/simulations/helpers.py \ + openfisca_core/simulations/types.py \ + openfisca_core/types.py @$(call print_pass,$@:) ## Run code formatters to correct style errors. diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index d84956ea5c..ef5a20220a 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -37,7 +37,7 @@ test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 openfisca_core/holders \ openfisca_core/periods \ openfisca_core/projectors \ - openfisca_core/types + openfisca_core/simulations @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ coverage run -m \ ${openfisca} test $? \ diff --git a/setup.cfg b/setup.cfg index 9673496d71..140d161769 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,82 +1,86 @@ -# C011X: We (progressively) document the code base. -# D10X: We (progressively) check docstrings (see https://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping). -# DARXXX: We (progressively) check docstrings (see https://github.com/terrencepreilly/darglint#error-codes). -# E203: We ignore a false positive in whitespace before ":" (see https://github.com/PyCQA/pycodestyle/issues/373). -# F403/405: We ignore * imports. -# R0401: We avoid cyclic imports —required for unit/doc tests. -# RST301: We use Google Python Style (see https://pypi.org/project/flake8-rst-docstrings/). -# W503/504: We break lines before binary operators (Knuth's style). +# C011X: We (progressively) document the code base. +# D10X: We (progressively) check docstrings (see https://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping). +# DARXXX: We (progressively) check docstrings (see https://github.com/terrencepreilly/darglint#error-codes). +# E203: We ignore a false positive in whitespace before ":" (see https://github.com/PyCQA/pycodestyle/issues/373). +# F403/405: We ignore * imports. +# R0401: We avoid cyclic imports —required for unit/doc tests. +# RST301: We use Google Python Style (see https://pypi.org/project/flake8-rst-docstrings/). +# W503/504: We break lines before binary operators (Knuth's style). [flake8] -convention = google -docstring_style = google -extend-ignore = D -ignore = E203, E501, F405, RST301, W503 -in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors openfisca_core/types -max-line-length = 88 -per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 -rst-directives = attribute, deprecated, seealso, versionadded, versionchanged -rst-roles = any, attr, class, exc, func, meth, mod, obj -strictness = short +convention = google +docstring_style = google +extend-ignore = D +ignore = E203, E501, F405, RST301, W503 +in-place = true +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors openfisca_core/simulations +max-line-length = 88 +per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 +rst-directives = attribute, deprecated, seealso, versionadded, versionchanged +rst-roles = any, attr, class, exc, func, meth, mod, obj +strictness = short [pylint.MASTER] -load-plugins = pylint_per_file_ignores +load-plugins = pylint_per_file_ignores [pylint.message_control] -disable = all -enable = C0115, C0116, R0401 -per-file-ignores = +disable = all +enable = C0115, C0116, R0401 +per-file-ignores = types.py:C0115,C0116 /tests/:C0116 -score = no +score = no [isort] -case_sensitive = true +case_sensitive = true force_alphabetical_sort_within_sections = false -group_by_package = true -honor_noqa = true -include_trailing_comma = true -known_first_party = openfisca_core -known_openfisca = openfisca_country_template, openfisca_extension_template -known_typing = *collections.abc*, *typing*, *typing_extensions* -known_types = *types* -profile = black -py_version = 39 -sections = FUTURE, TYPING, TYPES, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER +group_by_package = true +honor_noqa = true +include_trailing_comma = true +known_first_party = openfisca_core +known_openfisca = openfisca_country_template, openfisca_extension_template +known_typing = *collections.abc*, *typing*, *typing_extensions* +known_types = *types* +profile = black +py_version = 39 +sections = FUTURE, TYPING, TYPES, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER [coverage:paths] -source = . */site-packages +source = . */site-packages [coverage:run] -branch = true -source = openfisca_core, openfisca_web_api +branch = true +source = openfisca_core, openfisca_web_api [coverage:report] -fail_under = 75 -show_missing = true -skip_covered = true -skip_empty = true +fail_under = 75 +show_missing = true +skip_covered = true +skip_empty = true [tool:pytest] -addopts = --doctest-modules --disable-pytest-warnings --showlocals -doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE -python_files = **/*.py -testpaths = tests +addopts = --disable-pytest-warnings --doctest-modules --showlocals +doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE +python_files = **/*.py +testpaths = tests [mypy] -ignore_missing_imports = True -install_types = True -non_interactive = True +check_untyped_defs = false +disallow_any_decorated = false +disallow_any_explicit = false +disallow_any_expr = false +disallow_any_unimported = false +follow_imports = skip +ignore_missing_imports = true +implicit_reexport = false +install_types = true +non_interactive = true +plugins = numpy.typing.mypy_plugin +pretty = true +python_version = 3.9 +strict = false +warn_no_return = true +warn_unreachable = true -[mypy-openfisca_core.commons.tests.*] -ignore_errors = True - -[mypy-openfisca_core.holders.tests.*] -ignore_errors = True - -[mypy-openfisca_core.periods.tests.*] -ignore_errors = True - -[mypy-openfisca_core.scripts.*] -ignore_errors = True +[mypy-openfisca_core.*.tests.*] +ignore_errors = True diff --git a/setup.py b/setup.py index e822e138bc..92971f40bc 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.5.3", + version="41.5.4", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ diff --git a/tests/core/variables/test_definition_period.py b/tests/core/variables/test_definition_period.py deleted file mode 100644 index 7938aaeaef..0000000000 --- a/tests/core/variables/test_definition_period.py +++ /dev/null @@ -1,43 +0,0 @@ -import pytest - -from openfisca_core import periods -from openfisca_core.variables import Variable - - -@pytest.fixture -def variable(persons): - class TestVariable(Variable): - value_type = float - entity = persons - - return TestVariable - - -def test_weekday_variable(variable): - variable.definition_period = periods.WEEKDAY - assert variable() - - -def test_week_variable(variable): - variable.definition_period = periods.WEEK - assert variable() - - -def test_day_variable(variable): - variable.definition_period = periods.DAY - assert variable() - - -def test_month_variable(variable): - variable.definition_period = periods.MONTH - assert variable() - - -def test_year_variable(variable): - variable.definition_period = periods.YEAR - assert variable() - - -def test_eternity_variable(variable): - variable.definition_period = periods.ETERNITY - assert variable()