Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add weeks #1146

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
17 changes: 8 additions & 9 deletions openfisca_core/commons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
to OpenFisca Core and to country packages.

Official Public API:
* :class:`.either`
* :func:`.apply_thresholds`
* :func:`.average_rate`
* :func:`.concat`
Expand Down Expand Up @@ -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__]
116 changes: 116 additions & 0 deletions openfisca_core/commons/_adts.py
Original file line number Diff line number Diff line change
@@ -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."""
20 changes: 10 additions & 10 deletions openfisca_core/commons/formulas.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()))
4 changes: 2 additions & 2 deletions openfisca_core/commons/misc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TypeVar
from typing import Any, TypeVar, Union

from openfisca_core.types import Array

Expand Down Expand Up @@ -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:
Expand Down
14 changes: 7 additions & 7 deletions openfisca_core/commons/rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -41,7 +41,7 @@ def average_rate(

"""

average_rate: Array[float]
average_rate: Array[numpy.float_]

average_rate = 1 - target / varying

Expand All @@ -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
Expand Down Expand Up @@ -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:])

Expand Down
33 changes: 33 additions & 0 deletions openfisca_core/commons/tests/test_adts.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 11 additions & 1 deletion openfisca_core/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
8 changes: 6 additions & 2 deletions openfisca_core/entities/_core_entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from abc import abstractmethod

from .role import Role
from .types import Entity
from .types import CoreEntity


class _CoreEntity:
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions openfisca_core/entities/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import dataclasses
import textwrap

from .types import Entity
from .types import SingleEntity


class Role:
Expand Down Expand Up @@ -48,7 +48,7 @@ class Role:
"""

#: The Entity the Role belongs to.
entity: Entity
entity: SingleEntity

#: A description of the Role.
description: _Description
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading