Skip to content

Commit 632aec1

Browse files
committed
chore(git): merge & rebase
1 parent 2d2c50c commit 632aec1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+1505
-646
lines changed

openfisca_core/commons/__init__.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
to OpenFisca Core and to country packages.
55
66
Official Public API:
7+
* :class:`.either`
78
* :func:`.apply_thresholds`
89
* :func:`.average_rate`
910
* :func:`.concat`
@@ -50,18 +51,16 @@
5051
5152
"""
5253

53-
# Official Public API
54+
from ._adts import Either, Failure, Success
55+
from .dummy import Dummy # Deprecated
56+
from .formulas import apply_thresholds, concat, switch
57+
from .misc import empty_clone, stringify_array
58+
from .rates import average_rate, marginal_rate
5459

55-
from .formulas import apply_thresholds, concat, switch # noqa: F401
56-
from .misc import empty_clone, stringify_array # noqa: F401
57-
from .rates import average_rate, marginal_rate # noqa: F401
60+
either = Either
5861

5962
__all__ = ["apply_thresholds", "concat", "switch"]
6063
__all__ = ["empty_clone", "stringify_array", *__all__]
6164
__all__ = ["average_rate", "marginal_rate", *__all__]
62-
63-
# Deprecated
64-
65-
from .dummy import Dummy # noqa: F401
66-
65+
__all__ = ["Either", "Failure", "Success", "either", *__all__]
6766
__all__ = ["Dummy", *__all__]

openfisca_core/commons/_adts.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Algebraic data types for OpenFisca.
2+
3+
An algebraic data type is a structured type that’s formed by composing other
4+
types. [...] Product types allow you to have more than one value in a single
5+
structure, at the same time. [...] Sum types are types where your value must
6+
be one of a fixed set of options.
7+
8+
.. _See:
9+
https://jrsinclair.com/articles/2019/algebraic-data-types-what-i-wish-someone-had-explained-about-functional-programming/
10+
11+
"""
12+
13+
from __future__ import annotations
14+
15+
from collections.abc import Callable
16+
from typing import Generic, TypeVar, cast, final
17+
from typing_extensions import Never
18+
19+
import dataclasses
20+
21+
#: Type variable representing an error.
22+
E = TypeVar("E")
23+
24+
#: Type variable representing a value.
25+
A = TypeVar("A")
26+
27+
28+
@dataclasses.dataclass(frozen=True)
29+
class Either(Generic[E, A]):
30+
"""The Either monad.
31+
32+
The Either monad specifies the Either data type as well as several
33+
functions that operate on top of it. The Either data type represents the
34+
result of a computation that may fail.
35+
36+
"""
37+
38+
#: The value or state passed on.
39+
_value: E | A
40+
41+
@property
42+
@final
43+
def is_failure(self) -> bool:
44+
"""bool: Whether this instance represents a failure."""
45+
return isinstance(self, Failure)
46+
47+
@property
48+
@final
49+
def is_success(self) -> bool:
50+
"""bool: Whether this instance represents a success."""
51+
return isinstance(self, Success)
52+
53+
@final
54+
def unwrap(self) -> E | A:
55+
"""Return the value of this instance.
56+
57+
Examples:
58+
>>> Either.fail("error").unwrap()
59+
'error'
60+
61+
>>> Either.succeed(1).unwrap()
62+
1
63+
64+
Returns:
65+
E | A: The value of this instance.
66+
67+
"""
68+
69+
return self._value
70+
71+
@final
72+
def then(
73+
self, f: Callable[[A], Failure[E] | Success[A]]
74+
) -> Failure[E] | Success[A]:
75+
"""Apply a flatMap to input stream.
76+
77+
Examples:
78+
>>> Either.fail("error").then(lambda x: Either.succeed(x)).unwrap()
79+
'error'
80+
81+
>>> Either.succeed(1).then(lambda x: Either.succeed(x + 1)).unwrap()
82+
2
83+
84+
Args:
85+
f: A function that takes a value and returns a new Either instance.
86+
87+
Returns:
88+
_Failure[E] | _Success[A]: The result of applying f.
89+
90+
"""
91+
92+
if self.is_success:
93+
return f(cast(A, self.unwrap()))
94+
return Either.fail(cast(E, self.unwrap()))
95+
96+
@staticmethod
97+
@final
98+
def fail(value: E) -> Failure[E]:
99+
"""_Failure[E]: Create a failing result."""
100+
return Failure(value)
101+
102+
@staticmethod
103+
@final
104+
def succeed(value: A) -> Success[A]:
105+
"""_Success[A]: Create a successful result."""
106+
return Success(value)
107+
108+
109+
@final
110+
class Failure(Either[E, Never]):
111+
"""A failing result in an Either ADT."""
112+
113+
114+
@final
115+
class Success(Either[Never, A]):
116+
"""A successful result in an Either ADT."""

openfisca_core/commons/formulas.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1+
from typing import Any, Dict, Union
2+
13
from openfisca_core.types import Array, ArrayLike
2-
from typing import Any, Dict, Sequence, TypeVar
34

45
import numpy
56

6-
T = TypeVar("T")
7-
87

98
def apply_thresholds(
10-
input: Array[float],
9+
input: Array[numpy.float_],
1110
thresholds: ArrayLike[float],
1211
choices: ArrayLike[float],
13-
) -> Array[float]:
12+
) -> Array[numpy.float_]:
1413
"""Makes a choice based on an input and thresholds.
1514
1615
From a list of ``choices``, this function selects one of these values
@@ -39,7 +38,7 @@ def apply_thresholds(
3938
4039
"""
4140

42-
condlist: Sequence[Array[bool]]
41+
condlist: list[Union[Array[numpy.bool_], bool]]
4342
condlist = [input <= threshold for threshold in thresholds]
4443

4544
if len(condlist) == len(choices) - 1:
@@ -57,7 +56,9 @@ def apply_thresholds(
5756
return numpy.select(condlist, choices)
5857

5958

60-
def concat(this: ArrayLike[str], that: ArrayLike[str]) -> Array[str]:
59+
def concat(
60+
this: Union[Array[Any], ArrayLike[str]], that: Union[Array[Any], ArrayLike[str]]
61+
) -> Array[numpy.str_]:
6162
"""Concatenates the values of two arrays.
6263
6364
Args:
@@ -87,8 +88,8 @@ def concat(this: ArrayLike[str], that: ArrayLike[str]) -> Array[str]:
8788

8889
def switch(
8990
conditions: Array[Any],
90-
value_by_condition: Dict[float, T],
91-
) -> Array[T]:
91+
value_by_condition: Dict[float, Any],
92+
) -> Array[Any]:
9293
"""Mimicks a switch statement.
9394
9495
Given an array of conditions, returns an array of the same size,
@@ -119,4 +120,4 @@ def switch(
119120

120121
condlist = [conditions == condition for condition in value_by_condition.keys()]
121122

122-
return numpy.select(condlist, value_by_condition.values())
123+
return numpy.select(condlist, tuple(value_by_condition.values()))

openfisca_core/commons/misc.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
from typing import Any, TypeVar, Union
2+
13
from openfisca_core.types import Array
2-
from typing import TypeVar
34

45
T = TypeVar("T")
56

@@ -42,7 +43,7 @@ def empty_clone(original: T) -> T:
4243
return new
4344

4445

45-
def stringify_array(array: Array) -> str:
46+
def stringify_array(array: Union[Array[Any], None]) -> str:
4647
"""Generates a clean string representation of a numpy array.
4748
4849
Args:

openfisca_core/commons/rates.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
from openfisca_core.types import Array, ArrayLike
21
from typing import Optional
32

3+
from openfisca_core.types import Array, ArrayLike
4+
45
import numpy
56

67

78
def average_rate(
8-
target: Array[float],
9+
target: Array[numpy.float_],
910
varying: ArrayLike[float],
1011
trim: Optional[ArrayLike[float]] = None,
11-
) -> Array[float]:
12+
) -> Array[numpy.float_]:
1213
"""Computes the average rate of a target net income.
1314
1415
Given a ``target`` net income, and according to the ``varying`` gross
@@ -40,7 +41,7 @@ def average_rate(
4041
4142
"""
4243

43-
average_rate: Array[float]
44+
average_rate: Array[numpy.float_]
4445

4546
average_rate = 1 - target / varying
4647

@@ -61,10 +62,10 @@ def average_rate(
6162

6263

6364
def marginal_rate(
64-
target: Array[float],
65-
varying: Array[float],
65+
target: Array[numpy.float_],
66+
varying: Array[numpy.float_],
6667
trim: Optional[ArrayLike[float]] = None,
67-
) -> Array[float]:
68+
) -> Array[numpy.float_]:
6869
"""Computes the marginal rate of a target net income.
6970
7071
Given a ``target`` net income, and according to the ``varying`` gross
@@ -96,7 +97,7 @@ def marginal_rate(
9697
9798
"""
9899

99-
marginal_rate: Array[float]
100+
marginal_rate: Array[numpy.float_]
100101

101102
marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:])
102103

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import pytest
2+
3+
from openfisca_core import commons
4+
5+
6+
@pytest.fixture
7+
def failure():
8+
return commons.either.fail("error")
9+
10+
11+
@pytest.fixture
12+
def success():
13+
return commons.either.succeed(1)
14+
15+
16+
def test_either_is_failure(failure):
17+
assert failure.is_failure
18+
19+
20+
def test_either_is_success(success):
21+
assert success.is_success
22+
23+
24+
def test_either_unwrap(failure):
25+
assert failure.unwrap() == "error"
26+
27+
28+
def test_either_then(failure, success):
29+
assert failure.then(lambda x: failure).unwrap() == "error"
30+
assert failure.then(lambda x: success).unwrap() == "error"
31+
assert success.then(lambda x: failure).unwrap() == "error"
32+
assert success.then(lambda x: success).unwrap() == 1
33+
assert success.then(lambda x: commons.either.succeed(x + 1)).unwrap() == 2

openfisca_core/data_storage/in_memory_storage.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import numpy
22

33
from openfisca_core import periods
4-
from openfisca_core.periods import DateUnit
54

65

76
class InMemoryStorage:
@@ -15,7 +14,7 @@ def __init__(self, is_eternal=False):
1514

1615
def get(self, period):
1716
if self.is_eternal:
18-
period = periods.period(DateUnit.ETERNITY)
17+
period = periods.period(periods.ETERNITY)
1918
period = periods.period(period)
2019

2120
values = self._arrays.get(period)
@@ -25,7 +24,7 @@ def get(self, period):
2524

2625
def put(self, value, period):
2726
if self.is_eternal:
28-
period = periods.period(DateUnit.ETERNITY)
27+
period = periods.period(periods.ETERNITY)
2928
period = periods.period(period)
3029

3130
self._arrays[period] = value
@@ -36,7 +35,7 @@ def delete(self, period=None):
3635
return
3736

3837
if self.is_eternal:
39-
period = periods.period(DateUnit.ETERNITY)
38+
period = periods.period(periods.ETERNITY)
4039
period = periods.period(period)
4140

4241
self._arrays = {

openfisca_core/data_storage/on_disk_storage.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from openfisca_core import periods
77
from openfisca_core.indexed_enums import EnumArray
8-
from openfisca_core.periods import DateUnit
98

109

1110
class OnDiskStorage:
@@ -29,7 +28,7 @@ def _decode_file(self, file):
2928

3029
def get(self, period):
3130
if self.is_eternal:
32-
period = periods.period(DateUnit.ETERNITY)
31+
period = periods.period(periods.ETERNITY)
3332
period = periods.period(period)
3433

3534
values = self._files.get(period)
@@ -39,7 +38,7 @@ def get(self, period):
3938

4039
def put(self, value, period):
4140
if self.is_eternal:
42-
period = periods.period(DateUnit.ETERNITY)
41+
period = periods.period(periods.ETERNITY)
4342
period = periods.period(period)
4443

4544
filename = str(period)
@@ -56,7 +55,7 @@ def delete(self, period=None):
5655
return
5756

5857
if self.is_eternal:
59-
period = periods.period(DateUnit.ETERNITY)
58+
period = periods.period(periods.ETERNITY)
6059
period = periods.period(period)
6160

6261
if period is not None:

0 commit comments

Comments
 (0)