Skip to content

Commit 6c05349

Browse files
authored
Extend numpy support to v1.20
Add support for M1 processors
2 parents 3123d27 + 15dd1ff commit 6c05349

22 files changed

Lines changed: 615 additions & 224 deletions

CHANGELOG.md

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

3+
### 35.3.7 [#990](https://github.com/openfisca/openfisca-core/pull/990)
4+
5+
#### Technical changes
6+
7+
- Update dependencies.
8+
- Extend NumPy compatibility to v1.20 to support M1 processors.
9+
310
### 35.3.6 [#984](https://github.com/openfisca/openfisca-core/pull/984)
411

512
#### Technical changes

openfisca_core/indexed_enums/enum.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,47 @@
77

88
from openfisca_core.indexed_enums import config, EnumArray
99

10+
if typing.TYPE_CHECKING:
11+
IndexedEnumArray = numpy.object_
12+
1013

1114
class Enum(enum.Enum):
1215
"""
13-
Enum based on `enum34 <https://pypi.python.org/pypi/enum34/>`_, whose items have an
14-
index.
16+
Enum based on `enum34 <https://pypi.python.org/pypi/enum34/>`_, whose items
17+
have an index.
1518
"""
1619

1720
# Tweak enums to add an index attribute to each enum item
1821
def __init__(self, name: str) -> None:
19-
# When the enum item is initialized, self._member_names_ contains the names of
20-
# the previously initialized items, so its length is the index of this item.
22+
# When the enum item is initialized, self._member_names_ contains the
23+
# names of the previously initialized items, so its length is the index
24+
# of this item.
2125
self.index = len(self._member_names_)
2226

2327
# Bypass the slow Enum.__eq__
2428
__eq__ = object.__eq__
2529

26-
# In Python 3, __hash__ must be defined if __eq__ is defined to stay hashable.
30+
# In Python 3, __hash__ must be defined if __eq__ is defined to stay
31+
# hashable.
2732
__hash__ = object.__hash__
2833

2934
@classmethod
3035
def encode(
3136
cls,
3237
array: typing.Union[
3338
EnumArray,
34-
numpy.ndarray[int],
35-
numpy.ndarray[str],
36-
numpy.ndarray[Enum],
39+
numpy.int_,
40+
numpy.float_,
41+
IndexedEnumArray,
3742
],
3843
) -> EnumArray:
3944
"""
40-
Encode a string numpy array, an enum item numpy array, or an int numpy array
41-
into an :any:`EnumArray`. See :any:`EnumArray.decode` for decoding.
45+
Encode a string numpy array, an enum item numpy array, or an int numpy
46+
array into an :any:`EnumArray`. See :any:`EnumArray.decode` for
47+
decoding.
4248
43-
:param ndarray array: Array of string identifiers, or of enum items, to encode.
49+
:param ndarray array: Array of string identifiers, or of enum items, to
50+
encode.
4451
4552
:returns: An :any:`EnumArray` encoding the input array values.
4653
:rtype: :any:`EnumArray`
@@ -59,24 +66,31 @@ def encode(
5966
>>> encoded_array[0]
6067
2 # Encoded value
6168
"""
62-
if type(array) is EnumArray:
69+
if isinstance(array, EnumArray):
6370
return array
6471

65-
if array.dtype.kind in {'U', 'S'}: # String array
72+
# String array
73+
if isinstance(array, numpy.ndarray) and \
74+
array.dtype.kind in {'U', 'S'}:
6675
array = numpy.select(
6776
[array == item.name for item in cls],
6877
[item.index for item in cls],
6978
).astype(config.ENUM_ARRAY_DTYPE)
7079

71-
elif array.dtype.kind == 'O': # Enum items arrays
80+
# Enum items arrays
81+
elif isinstance(array, numpy.ndarray) and \
82+
array.dtype.kind == 'O':
7283
# Ensure we are comparing the comparable. The problem this fixes:
7384
# On entering this method "cls" will generally come from
74-
# variable.possible_values, while the array values may come from directly
75-
# importing a module containing an Enum class. However, variables (and
76-
# hence their possible_values) are loaded by a call to load_module, which
77-
# gives them a different identity from the ones imported in the usual way.
78-
# So, instead of relying on the "cls" passed in, we use only its name to
79-
# check that the values in the array, if non-empty, are of the right type.
85+
# variable.possible_values, while the array values may come from
86+
# directly importing a module containing an Enum class. However,
87+
# variables (and hence their possible_values) are loaded by a call
88+
# to load_module, which gives them a different identity from the
89+
# ones imported in the usual way.
90+
#
91+
# So, instead of relying on the "cls" passed in, we use only its
92+
# name to check that the values in the array, if non-empty, are of
93+
# the right type.
8094
if len(array) > 0 and cls.__name__ is array[0].__class__.__name__:
8195
cls = array[0].__class__
8296

openfisca_core/indexed_enums/enum_array.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
if typing.TYPE_CHECKING:
88
from openfisca_core.indexed_enums import Enum
99

10+
IndexedEnumArray = numpy.object_
11+
1012

1113
class EnumArray(numpy.ndarray):
1214
"""
@@ -20,23 +22,23 @@ class EnumArray(numpy.ndarray):
2022
# https://docs.scipy.org/doc/numpy-1.13.0/user/basics.subclassing.html#slightly-more-realistic-example-attribute-added-to-existing-array.
2123
def __new__(
2224
cls,
23-
input_array: numpy.ndarray[int],
25+
input_array: numpy.int_,
2426
possible_values: typing.Optional[typing.Type[Enum]] = None,
2527
) -> EnumArray:
2628
obj = numpy.asarray(input_array).view(cls)
2729
obj.possible_values = possible_values
2830
return obj
2931

3032
# See previous comment
31-
def __array_finalize__(self, obj: typing.Optional[numpy.ndarray[int]]) -> None:
33+
def __array_finalize__(self, obj: typing.Optional[numpy.int_]) -> None:
3234
if obj is None:
3335
return
3436

3537
self.possible_values = getattr(obj, "possible_values", None)
3638

3739
def __eq__(self, other: typing.Any) -> bool:
38-
# When comparing to an item of self.possible_values, use the item index to
39-
# speed up the comparison.
40+
# When comparing to an item of self.possible_values, use the item index
41+
# to speed up the comparison.
4042
if other.__class__.__name__ is self.possible_values.__name__:
4143
# Use view(ndarray) so that the result is a classic ndarray, not an
4244
# EnumArray.
@@ -49,8 +51,8 @@ def __ne__(self, other: typing.Any) -> bool:
4951

5052
def _forbidden_operation(self, other: typing.Any) -> typing.NoReturn:
5153
raise TypeError(
52-
"Forbidden operation. The only operations allowed on EnumArrays are "
53-
"'==' and '!='.",
54+
"Forbidden operation. The only operations allowed on EnumArrays "
55+
"are '==' and '!='.",
5456
)
5557

5658
__add__ = _forbidden_operation
@@ -62,7 +64,7 @@ def _forbidden_operation(self, other: typing.Any) -> typing.NoReturn:
6264
__and__ = _forbidden_operation
6365
__or__ = _forbidden_operation
6466

65-
def decode(self) -> numpy.ndarray[Enum]:
67+
def decode(self) -> IndexedEnumArray:
6668
"""
6769
Return the array of enum items corresponding to self.
6870
@@ -72,14 +74,16 @@ def decode(self) -> numpy.ndarray[Enum]:
7274
>>> enum_array[0]
7375
>>> 2 # Encoded value
7476
>>> enum_array.decode()[0]
75-
<HousingOccupancyStatus.free_lodger: 'Free lodger'> # Decoded value : enum item
77+
<HousingOccupancyStatus.free_lodger: 'Free lodger'>
78+
79+
Decoded value: enum item
7680
"""
7781
return numpy.select(
7882
[self == item.index for item in self.possible_values],
7983
list(self.possible_values),
8084
)
8185

82-
def decode_to_str(self) -> numpy.ndarray[str]:
86+
def decode_to_str(self) -> numpy.str_:
8387
"""
8488
Return the array of string identifiers corresponding to self.
8589

openfisca_core/taxscales/abstract_rate_tax_scale.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,36 @@
33
import typing
44
import warnings
55

6-
import numpy
7-
86
from openfisca_core.taxscales import RateTaxScaleLike
97

8+
if typing.TYPE_CHECKING:
9+
import numpy
10+
11+
NumericalArray = typing.Union[numpy.int_, numpy.float_]
12+
1013

1114
class AbstractRateTaxScale(RateTaxScaleLike):
1215
"""
13-
Base class for various types of rate-based tax scales: marginal rate, linear
14-
average rate...
16+
Base class for various types of rate-based tax scales: marginal rate,
17+
linear average rate...
1518
"""
1619

17-
def __init__(self, name: typing.Optional[str] = None, option = None, unit = None) -> None:
20+
def __init__(
21+
self, name: typing.Optional[str] = None,
22+
option: typing.Any = None,
23+
unit: typing.Any = None,
24+
) -> None:
1825
message = [
19-
"The 'AbstractRateTaxScale' class has been deprecated since version",
20-
"34.7.0, and will be removed in the future.",
26+
"The 'AbstractRateTaxScale' class has been deprecated since",
27+
"version 34.7.0, and will be removed in the future.",
2128
]
29+
2230
warnings.warn(" ".join(message), DeprecationWarning)
23-
super(AbstractRateTaxScale, self).__init__(name, option, unit)
31+
super().__init__(name, option, unit)
2432

2533
def calc(
2634
self,
27-
tax_base: typing.Union[numpy.ndarray[int], numpy.ndarray[float]],
35+
tax_base: NumericalArray,
2836
right: bool,
2937
) -> typing.NoReturn:
3038
raise NotImplementedError(

openfisca_core/taxscales/abstract_tax_scale.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,34 @@
33
import typing
44
import warnings
55

6-
import numpy
7-
86
from openfisca_core.taxscales import TaxScaleLike
97

8+
if typing.TYPE_CHECKING:
9+
import numpy
10+
11+
NumericalArray = typing.Union[numpy.int_, numpy.float_]
12+
1013

1114
class AbstractTaxScale(TaxScaleLike):
1215
"""
13-
Base class for various types of tax scales: amount-based tax scales, rate-based
14-
tax scales...
16+
Base class for various types of tax scales: amount-based tax scales,
17+
rate-based tax scales...
1518
"""
1619

17-
def __init__(self, name: typing.Optional[str] = None, option = None, unit = None) -> None:
20+
def __init__(
21+
self,
22+
name: typing.Optional[str] = None,
23+
option: typing.Any = None,
24+
unit: numpy.int_ = None,
25+
) -> None:
26+
1827
message = [
19-
"The 'AbstractTaxScale' class has been deprecated since version 34.7.0,",
20-
"and will be removed in the future.",
28+
"The 'AbstractTaxScale' class has been deprecated since",
29+
"version 34.7.0, and will be removed in the future.",
2130
]
31+
2232
warnings.warn(" ".join(message), DeprecationWarning)
23-
super(AbstractTaxScale, self).__init__(name, option, unit)
33+
super().__init__(name, option, unit)
2434

2535
def __repr__(self) -> typing.NoReturn:
2636
raise NotImplementedError(
@@ -30,7 +40,7 @@ def __repr__(self) -> typing.NoReturn:
3040

3141
def calc(
3242
self,
33-
tax_base: typing.Union[numpy.ndarray[int], numpy.ndarray[float]],
43+
tax_base: NumericalArray,
3444
right: bool,
3545
) -> typing.NoReturn:
3646
raise NotImplementedError(

openfisca_core/taxscales/amount_tax_scale_like.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,28 @@
99

1010
class AmountTaxScaleLike(TaxScaleLike, abc.ABC):
1111
"""
12-
Base class for various types of amount-based tax scales: single amount, marginal
13-
amount...
12+
Base class for various types of amount-based tax scales: single amount,
13+
marginal amount...
1414
"""
1515

1616
amounts: typing.List
1717

18-
def __init__(self, name: typing.Optional[str] = None, option = None, unit = None) -> None:
19-
super(AmountTaxScaleLike, self).__init__(name, option, unit)
18+
def __init__(
19+
self,
20+
name: typing.Optional[str] = None,
21+
option: typing.Any = None,
22+
unit: typing.Any = None,
23+
) -> None:
24+
super().__init__(name, option, unit)
2025
self.amounts = []
2126

2227
def __repr__(self) -> str:
2328
return tools.indent(
2429
os.linesep.join(
2530
[
2631
f"- threshold: {threshold}{os.linesep} amount: {amount}"
27-
for (threshold, amount) in zip(self.thresholds, self.amounts)
32+
for (threshold, amount)
33+
in zip(self.thresholds, self.amounts)
2834
]
2935
)
3036
)
@@ -37,6 +43,7 @@ def add_bracket(
3743
if threshold in self.thresholds:
3844
i = self.thresholds.index(threshold)
3945
self.amounts[i] += amount
46+
4047
else:
4148
i = bisect.bisect_left(self.thresholds, threshold)
4249
self.thresholds.insert(i, threshold)
@@ -45,5 +52,6 @@ def add_bracket(
4552
def to_dict(self) -> dict:
4653
return {
4754
str(threshold): self.amounts[index]
48-
for index, threshold in enumerate(self.thresholds)
55+
for index, threshold
56+
in enumerate(self.thresholds)
4957
}

openfisca_core/taxscales/helpers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
if typing.TYPE_CHECKING:
1111
from openfisca_core.parameters import ParameterNodeAtInstant
1212

13+
TaxScales = typing.Optional[taxscales.MarginalRateTaxScale]
14+
1315

1416
def combine_tax_scales(
1517
node: ParameterNodeAtInstant,
16-
combined_tax_scales: typing.Optional[taxscales.MarginalRateTaxScale] = None,
17-
) -> typing.Optional[taxscales.MarginalRateTaxScale]:
18+
combined_tax_scales: TaxScales = None,
19+
) -> TaxScales:
1820
"""
1921
Combine all the MarginalRateTaxScales in the node into a single
2022
MarginalRateTaxScale.

openfisca_core/taxscales/linear_average_rate_tax_scale.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@
1010

1111
log = logging.getLogger(__name__)
1212

13+
if typing.TYPE_CHECKING:
14+
NumericalArray = typing.Union[numpy.int_, numpy.float_]
15+
1316

1417
class LinearAverageRateTaxScale(RateTaxScaleLike):
1518

1619
def calc(
1720
self,
18-
tax_base: typing.Union[numpy.ndarray[int], numpy.ndarray[float]],
21+
tax_base: NumericalArray,
1922
right: bool = False,
20-
) -> numpy.ndarray[float]:
23+
) -> numpy.float_:
2124
if len(self.rates) == 1:
2225
return tax_base * self.rates[0]
2326

0 commit comments

Comments
 (0)