Skip to content

Commit 7d451a1

Browse files
authored
Document & test eval_expression (#1225)
2 parents eb154a0 + ecf904f commit 7d451a1

39 files changed

+402
-313
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
### 41.5.7 [#1225](https://github.com/openfisca/openfisca-core/pull/1225)
4+
5+
#### Technical changes
6+
7+
- Refactor & test `eval_expression`
8+
39
### 41.5.6 [#1185](https://github.com/openfisca/openfisca-core/pull/1185)
410

511
#### Technical changes

openfisca_core/commons/__init__.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* :func:`.average_rate`
99
* :func:`.concat`
1010
* :func:`.empty_clone`
11+
* :func:`.eval_expression`
1112
* :func:`.marginal_rate`
1213
* :func:`.stringify_array`
1314
* :func:`.switch`
@@ -50,18 +51,21 @@
5051
5152
"""
5253

53-
# Official Public API
54-
54+
from . import types
55+
from .dummy import Dummy
5556
from .formulas import apply_thresholds, concat, switch
56-
from .misc import empty_clone, stringify_array
57+
from .misc import empty_clone, eval_expression, stringify_array
5758
from .rates import average_rate, marginal_rate
5859

59-
__all__ = ["apply_thresholds", "concat", "switch"]
60-
__all__ = ["empty_clone", "stringify_array", *__all__]
61-
__all__ = ["average_rate", "marginal_rate", *__all__]
62-
63-
# Deprecated
64-
65-
from .dummy import Dummy
66-
67-
__all__ = ["Dummy", *__all__]
60+
__all__ = [
61+
"Dummy",
62+
"apply_thresholds",
63+
"average_rate",
64+
"concat",
65+
"empty_clone",
66+
"eval_expression",
67+
"marginal_rate",
68+
"stringify_array",
69+
"switch",
70+
"types",
71+
]

openfisca_core/commons/dummy.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ def __init__(self) -> None:
2020
"and will be removed in the future.",
2121
]
2222
warnings.warn(" ".join(message), DeprecationWarning, stacklevel=2)
23+
24+
25+
__all__ = ["Dummy"]

openfisca_core/commons/formulas.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1+
from __future__ import annotations
2+
13
from collections.abc import Mapping
2-
from typing import Union
34

45
import numpy
56

6-
from openfisca_core import types as t
7+
from . import types as t
78

89

910
def apply_thresholds(
10-
input: t.Array[numpy.float64],
11+
input: t.Array[numpy.float32],
1112
thresholds: t.ArrayLike[float],
1213
choices: t.ArrayLike[float],
13-
) -> t.Array[numpy.float64]:
14+
) -> t.Array[numpy.float32]:
1415
"""Makes a choice based on an input and thresholds.
1516
1617
From a list of ``choices``, this function selects one of these values
@@ -38,26 +39,29 @@ def apply_thresholds(
3839
array([10, 10, 15, 15, 20])
3940
4041
"""
41-
condlist: list[Union[t.Array[numpy.bool_], bool]]
42+
43+
condlist: list[t.Array[numpy.bool_] | bool]
4244
condlist = [input <= threshold for threshold in thresholds]
4345

4446
if len(condlist) == len(choices) - 1:
4547
# If a choice is provided for input > highest threshold, last condition
4648
# must be true to return it.
4749
condlist += [True]
4850

49-
assert len(condlist) == len(
50-
choices
51-
), "'apply_thresholds' must be called with the same number of thresholds than choices, or one more choice."
51+
msg = (
52+
"'apply_thresholds' must be called with the same number of thresholds "
53+
"than choices, or one more choice."
54+
)
55+
assert len(condlist) == len(choices), msg
5256

5357
return numpy.select(condlist, choices)
5458

5559

5660
def concat(
57-
this: Union[t.Array[numpy.str_], t.ArrayLike[str]],
58-
that: Union[t.Array[numpy.str_], t.ArrayLike[str]],
61+
this: t.Array[numpy.str_] | t.ArrayLike[str],
62+
that: t.Array[numpy.str_] | t.ArrayLike[str],
5963
) -> t.Array[numpy.str_]:
60-
"""Concatenates the values of two arrays.
64+
"""Concatenate the values of two arrays.
6165
6266
Args:
6367
this: An array to concatenate.
@@ -84,10 +88,10 @@ def concat(
8488

8589

8690
def switch(
87-
conditions: t.Array[numpy.float64],
91+
conditions: t.Array[numpy.float32],
8892
value_by_condition: Mapping[float, float],
89-
) -> t.Array[numpy.float64]:
90-
"""Mimicks a switch statement.
93+
) -> t.Array[numpy.float32]:
94+
"""Mimick a switch statement.
9195
9296
Given an array of conditions, returns an array of the same size,
9397
replacing each condition item with the matching given value.
@@ -117,3 +121,6 @@ def switch(
117121
condlist = [conditions == condition for condition in value_by_condition]
118122

119123
return numpy.select(condlist, tuple(value_by_condition.values()))
124+
125+
126+
__all__ = ["apply_thresholds", "concat", "switch"]

openfisca_core/commons/misc.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
from typing import Optional, TypeVar
1+
from __future__ import annotations
22

3+
import numexpr
34
import numpy
45

56
from openfisca_core import types as t
67

7-
T = TypeVar("T")
88

9-
10-
def empty_clone(original: T) -> T:
11-
"""Creates an empty instance of the same class of the original object.
9+
def empty_clone(original: object) -> object:
10+
"""Create an empty instance of the same class of the original object.
1211
1312
Args:
1413
original: An object to clone.
@@ -30,22 +29,20 @@ def empty_clone(original: T) -> T:
3029
True
3130
3231
"""
33-
Dummy: object
34-
new: T
3532

3633
Dummy = type(
3734
"Dummy",
3835
(original.__class__,),
39-
{"__init__": lambda self: None},
36+
{"__init__": lambda _: None},
4037
)
4138

4239
new = Dummy()
4340
new.__class__ = original.__class__
4441
return new
4542

4643

47-
def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str:
48-
"""Generates a clean string representation of a numpy array.
44+
def stringify_array(array: None | t.Array[numpy.generic]) -> str:
45+
"""Generate a clean string representation of a numpy array.
4946
5047
Args:
5148
array: An array.
@@ -76,3 +73,33 @@ def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str:
7673
return "None"
7774

7875
return f"[{', '.join(str(cell) for cell in array)}]"
76+
77+
78+
def eval_expression(
79+
expression: str,
80+
) -> str | t.Array[numpy.bool_] | t.Array[numpy.int32] | t.Array[numpy.float32]:
81+
"""Evaluate a string expression to a numpy array.
82+
83+
Args:
84+
expression(str): An expression to evaluate.
85+
86+
Returns:
87+
:obj:`object`: The result of the evaluation.
88+
89+
Examples:
90+
>>> eval_expression("1 + 2")
91+
array(3, dtype=int32)
92+
93+
>>> eval_expression("salary")
94+
'salary'
95+
96+
"""
97+
98+
try:
99+
return numexpr.evaluate(expression)
100+
101+
except (KeyError, TypeError):
102+
return expression
103+
104+
105+
__all__ = ["empty_clone", "eval_expression", "stringify_array"]

openfisca_core/commons/rates.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
from typing import Optional
2-
3-
from openfisca_core.types import Array, ArrayLike
1+
from __future__ import annotations
42

53
import numpy
64

5+
from . import types as t
6+
77

88
def average_rate(
9-
target: Array[numpy.float64],
10-
varying: ArrayLike[float],
11-
trim: Optional[ArrayLike[float]] = None,
12-
) -> Array[numpy.float64]:
13-
"""Computes the average rate of a target net income.
9+
target: t.Array[numpy.float32],
10+
varying: t.ArrayLike[float],
11+
trim: None | t.ArrayLike[float] = None,
12+
) -> t.Array[numpy.float32]:
13+
"""Compute the average rate of a target net income.
1414
1515
Given a ``target`` net income, and according to the ``varying`` gross
1616
income. Optionally, a ``trim`` can be applied consisting of the lower and
@@ -40,8 +40,8 @@ def average_rate(
4040
array([ nan, 0. , -0.5])
4141
4242
"""
43-
average_rate: Array[numpy.float64]
4443

44+
average_rate: t.Array[numpy.float32]
4545
average_rate = 1 - target / varying
4646

4747
if trim is not None:
@@ -61,11 +61,11 @@ def average_rate(
6161

6262

6363
def marginal_rate(
64-
target: Array[numpy.float64],
65-
varying: Array[numpy.float64],
66-
trim: Optional[ArrayLike[float]] = None,
67-
) -> Array[numpy.float64]:
68-
"""Computes the marginal rate of a target net income.
64+
target: t.Array[numpy.float32],
65+
varying: t.Array[numpy.float32],
66+
trim: None | t.ArrayLike[float] = None,
67+
) -> t.Array[numpy.float32]:
68+
"""Compute the marginal rate of a target net income.
6969
7070
Given a ``target`` net income, and according to the ``varying`` gross
7171
income. Optionally, a ``trim`` can be applied consisting of the lower and
@@ -95,8 +95,8 @@ def marginal_rate(
9595
array([nan, 0.5])
9696
9797
"""
98-
marginal_rate: Array[numpy.float64]
9998

99+
marginal_rate: t.Array[numpy.float32]
100100
marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:])
101101

102102
if trim is not None:
@@ -113,3 +113,6 @@ def marginal_rate(
113113
)
114114

115115
return marginal_rate
116+
117+
118+
__all__ = ["average_rate", "marginal_rate"]

openfisca_core/commons/types.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from openfisca_core.types import Array, ArrayLike
2+
3+
__all__ = ["Array", "ArrayLike"]
Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

3+
from typing import ClassVar
4+
5+
import abc
36
import os
4-
from abc import abstractmethod
57

68
from . import types as t
79
from .role import Role
@@ -12,29 +14,30 @@ class _CoreEntity:
1214

1315
#: A key to identify the entity.
1416
key: t.EntityKey
17+
1518
#: The ``key``, pluralised.
16-
plural: t.EntityPlural | None
19+
plural: t.EntityPlural
1720

1821
#: A summary description.
19-
label: str | None
22+
label: str
2023

2124
#: A full description.
22-
doc: str | None
25+
doc: str
2326

2427
#: Whether the entity is a person or not.
25-
is_person: bool
28+
is_person: ClassVar[bool]
2629

2730
#: A TaxBenefitSystem instance.
28-
_tax_benefit_system: t.TaxBenefitSystem | None = None
31+
_tax_benefit_system: None | t.TaxBenefitSystem = None
2932

30-
@abstractmethod
33+
@abc.abstractmethod
3134
def __init__(
3235
self,
33-
key: str,
34-
plural: str,
35-
label: str,
36-
doc: str,
37-
*args: object,
36+
__key: str,
37+
__plural: str,
38+
__label: str,
39+
__doc: str,
40+
*__args: object,
3841
) -> None: ...
3942

4043
def __repr__(self) -> str:
@@ -46,7 +49,7 @@ def set_tax_benefit_system(self, tax_benefit_system: t.TaxBenefitSystem) -> None
4649

4750
def get_variable(
4851
self,
49-
variable_name: str,
52+
variable_name: t.VariableName,
5053
check_existence: bool = False,
5154
) -> t.Variable | None:
5255
"""Get a ``variable_name`` from ``variables``."""
@@ -57,16 +60,20 @@ def get_variable(
5760
)
5861
return self._tax_benefit_system.get_variable(variable_name, check_existence)
5962

60-
def check_variable_defined_for_entity(self, variable_name: str) -> None:
63+
def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> None:
6164
"""Check if ``variable_name`` is defined for ``self``."""
62-
variable: t.Variable | None
63-
entity: t.CoreEntity
64-
65-
variable = self.get_variable(variable_name, check_existence=True)
65+
entity: None | t.CoreEntity = None
66+
variable: None | t.Variable = self.get_variable(
67+
variable_name,
68+
check_existence=True,
69+
)
6670

6771
if variable is not None:
6872
entity = variable.entity
6973

74+
if entity is None:
75+
return
76+
7077
if entity.key != self.key:
7178
message = (
7279
f"You tried to compute the variable '{variable_name}' for",
@@ -77,8 +84,12 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None:
7784
)
7885
raise ValueError(os.linesep.join(message))
7986

80-
def check_role_validity(self, role: object) -> None:
87+
@staticmethod
88+
def check_role_validity(role: object) -> None:
8189
"""Check if a ``role`` is an instance of Role."""
8290
if role is not None and not isinstance(role, Role):
8391
msg = f"{role} is not a valid role"
8492
raise ValueError(msg)
93+
94+
95+
__all__ = ["_CoreEntity"]

0 commit comments

Comments
 (0)