From 1b0d835ac7eeb9ebcac62ff5010fa6c00724f488 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 14:55:28 +0200 Subject: [PATCH 01/11] Add strict type-linting for commons --- openfisca_tasks/lint.mk | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 886072bb2b..115c6267bb 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 check-types +lint: check-syntax-errors check-style lint-doc check-types lint-typing-strict @$(call print_pass,$@:) ## Compile python files to check for syntax errors. @@ -39,6 +39,22 @@ check-types: @mypy --package openfisca_core --package openfisca_web_api @$(call print_pass,$@:) +## Run static type checkers for type errors (strict). +lint-typing-strict: \ + lint-typing-strict-commons \ + lint-typing-strict-types \ + ; + +## Run static type checkers for type errors (strict). +lint-typing-strict-%: + @$(call print_help,$(subst $*,%,$@:)) + @mypy \ + --cache-dir .mypy_cache-openfisca_core.$* \ + --implicit-reexport \ + --strict \ + --package openfisca_core.$* + @$(call print_pass,$@:) + ## Run code formatters to correct style errors. format-style: $(shell git ls-files "*.py") @$(call print_help,$@:) From ec78d711da56718671332f7c5a7a0b9b0c3002df Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 15:02:08 +0200 Subject: [PATCH 02/11] Add nptyping --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index acb886dde0..993b498bf1 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ general_requirements = [ 'dpath >= 1.5.0, < 2.0.0', + 'nptyping == 1.4.4', 'numexpr >= 2.7.0, <= 3.0', 'numpy >= 1.11, < 1.21', 'psutil >= 5.4.7, < 6.0.0', @@ -29,7 +30,7 @@ 'flake8-bugbear >= 19.3.0, < 20.0.0', 'flake8-docstrings == 1.6.0', 'flake8-print >= 3.1.0, < 4.0.0', - 'flake8-rst-docstrings < 1.0.0', + 'flake8-rst-docstrings == 0.2.3', 'mypy >= 0.701, < 0.800', 'openfisca-country-template >= 3.10.0, < 4.0.0', 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0', From e41220c8a5b4c72b36ab3265b813c14a46f27b82 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 15:03:26 +0200 Subject: [PATCH 03/11] Add typing extensions --- setup.cfg | 6 ++++-- setup.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 213867581a..108d152f59 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,9 +31,11 @@ testpaths = openfisca_core/commons tests [mypy] ignore_missing_imports = True +install_types = True +non_interactive = True [mypy-openfisca_core.commons.tests.*] -ignore_errors = True +ignore_errors = True [mypy-openfisca_core.scripts.*] -ignore_errors = True +ignore_errors = True diff --git a/setup.py b/setup.py index 993b498bf1..151f899e8c 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ 'pytest >= 4.4.1, < 6.0.0', # For openfisca test 'PyYAML >= 3.10', 'sortedcontainers == 2.2.2', + 'typing-extensions == 3.10.0.2', ] api_requirements = [ @@ -31,7 +32,7 @@ 'flake8-docstrings == 1.6.0', 'flake8-print >= 3.1.0, < 4.0.0', 'flake8-rst-docstrings == 0.2.3', - 'mypy >= 0.701, < 0.800', + 'mypy == 0.910', 'openfisca-country-template >= 3.10.0, < 4.0.0', 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0', 'pylint == 2.10.2', From 8ed1c4fb2248117984578fda743dd3616b8ee717 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 15:07:48 +0200 Subject: [PATCH 04/11] Add types to formulas --- openfisca_core/commons/formulas.py | 50 +++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 3a93477bc5..4d67c910ec 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,12 +1,20 @@ +from typing import Any, Dict, Sequence + import numpy +from openfisca_core.types import ArrayLike, ArrayType + -def apply_thresholds(input, thresholds, choices): +def apply_thresholds( + input: ArrayType[float], + thresholds: ArrayLike[float], + choices: ArrayLike[float], + ) -> ArrayType[float]: """Makes a choice based on an input and thresholds. - From a list of ``choices``, this function selects one of these values based on a list - of inputs, depending on the value of each ``input`` within a list of - ``thresholds``. + From a list of ``choices``, this function selects one of these values + based on a list of inputs, depending on the value of each ``input`` within + a list of ``thresholds``. Args: input: A list of inputs to make a choice from. @@ -30,16 +38,24 @@ def apply_thresholds(input, thresholds, choices): """ + condlist: Sequence[ArrayType[bool]] condlist = [input <= threshold for threshold in thresholds] + if len(condlist) == len(choices) - 1: - # If a choice is provided for input > highest threshold, last condition must be true to return it. + # If a choice is provided for input > highest threshold, last condition + # must be true to return it. condlist += [True] + assert len(condlist) == len(choices), \ - "apply_thresholds must be called with the same number of thresholds than choices, or one more choice" + " ".join([ + "'apply_thresholds' must be called with the same number of", + "thresholds than choices, or one more choice.", + ]) + return numpy.select(condlist, choices) -def concat(this, that): +def concat(this: ArrayLike[str], that: ArrayLike[str]) -> ArrayType[str]: """Concatenates the values of two arrays. Args: @@ -58,15 +74,23 @@ def concat(this, that): """ - if isinstance(this, numpy.ndarray) and not numpy.issubdtype(this.dtype, numpy.str): + if isinstance(this, numpy.ndarray) and \ + not numpy.issubdtype(this.dtype, numpy.str_): + this = this.astype('str') - if isinstance(that, numpy.ndarray) and not numpy.issubdtype(that.dtype, numpy.str): + + if isinstance(that, numpy.ndarray) and \ + not numpy.issubdtype(that.dtype, numpy.str_): + that = that.astype('str') - return numpy.core.defchararray.add(this, that) + return numpy.char.add(this, that) -def switch(conditions, value_by_condition): +def switch( + conditions: ArrayType[float], + value_by_condition: Dict[float, Any], + ) -> ArrayType[float]: """Mimicks a switch statement. Given an array of conditions, returns an array of the same size, @@ -92,9 +116,11 @@ def switch(conditions, value_by_condition): """ assert len(value_by_condition) > 0, \ - "switch must be called with at least one value" + "'switch' must be called with at least one value." + condlist = [ conditions == condition for condition in value_by_condition.keys() ] + return numpy.select(condlist, value_by_condition.values()) From fc6bd890678c88ea771c53d57d66022a4be4ce5f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 15:12:19 +0200 Subject: [PATCH 05/11] Add types to misc --- openfisca_core/commons/misc.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 4999e6cfe4..dd05cea11b 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,7 +1,11 @@ -import numpy +from typing import TypeVar +from openfisca_core.types import ArrayType -def empty_clone(original): +T = TypeVar("T") + + +def empty_clone(original: T) -> T: """Creates an empty instance of the same class of the original object. Args: @@ -25,18 +29,21 @@ def empty_clone(original): """ - class Dummy(original.__class__): - """Dummy class for empty cloning.""" + Dummy: object + new: T - def __init__(self) -> None: - ... + Dummy = type( + "Dummy", + (original.__class__,), + {"__init__": lambda self: None}, + ) new = Dummy() new.__class__ = original.__class__ return new -def stringify_array(array: numpy.ndarray) -> str: +def stringify_array(array: ArrayType) -> str: """Generates a clean string representation of a numpy array. Args: From 2206364643bad25c196b16c2b0a2f5add96f78f5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 15:14:17 +0200 Subject: [PATCH 06/11] Add types to rates --- openfisca_core/commons/rates.py | 57 +++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 7 deletions(-) diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index bb94e24776..af862a502d 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -1,7 +1,15 @@ +from typing import Optional + import numpy +from openfisca_core.types import ArrayLike, ArrayType + -def average_rate(target = None, varying = None, trim = None): +def average_rate( + target: ArrayType[float], + varying: ArrayLike[float], + trim: Optional[ArrayLike[float]] = None, + ) -> ArrayType[float]: """Computes the average rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -33,15 +41,32 @@ def average_rate(target = None, varying = None, trim = None): """ + average_rate: ArrayType[float] + average_rate = 1 - target / varying + if trim is not None: - average_rate = numpy.where(average_rate <= max(trim), average_rate, numpy.nan) - average_rate = numpy.where(average_rate >= min(trim), average_rate, numpy.nan) + + average_rate = numpy.where( + average_rate <= max(trim), + average_rate, + numpy.nan, + ) + + average_rate = numpy.where( + average_rate >= min(trim), + average_rate, + numpy.nan, + ) return average_rate -def marginal_rate(target = None, varying = None, trim = None): +def marginal_rate( + target: ArrayType[float], + varying: ArrayType[float], + trim: Optional[ArrayLike[float]] = None, + ) -> ArrayType[float]: """Computes the marginal rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -73,9 +98,27 @@ def marginal_rate(target = None, varying = None, trim = None): """ - marginal_rate = 1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) + marginal_rate: ArrayType[float] + + marginal_rate = ( + + 1 + - (target[:-1] + - target[1:]) / (varying[:-1] + - varying[1:]) + ) + if trim is not None: - marginal_rate = numpy.where(marginal_rate <= max(trim), marginal_rate, numpy.nan) - marginal_rate = numpy.where(marginal_rate >= min(trim), marginal_rate, numpy.nan) + + marginal_rate = numpy.where( + marginal_rate <= max(trim), + marginal_rate, + numpy.nan, + ) + + marginal_rate = numpy.where( + marginal_rate >= min(trim), + marginal_rate, + numpy.nan, + ) return marginal_rate From 54c25fd21b4f9944e75bf5d9f4f266452cba8de5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 30 Sep 2021 14:41:23 +0200 Subject: [PATCH 07/11] Correct marginal rate calculation Co-authored-by: Mahdi Ben Jelloul --- openfisca_core/commons/rates.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index af862a502d..d7d07ea1d8 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -102,9 +102,8 @@ def marginal_rate( marginal_rate = ( + 1 - - (target[:-1] - - target[1:]) / (varying[:-1] - - varying[1:]) + - (target[:-1] - target[1:]) + / (varying[:-1] - varying[1:]) ) if trim is not None: From c32fac4ca5a4ea1de5cc5f8e42606a7f9704dea1 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 30 Sep 2021 16:11:23 +0200 Subject: [PATCH 08/11] Fix switch type annotation --- openfisca_core/commons/formulas.py | 12 +++-- openfisca_core/commons/rates.py | 2 +- openfisca_core/types/__init__.py | 45 ++++++++++++++++++ openfisca_core/types/data_types/__init__.py | 1 + openfisca_core/types/data_types/arrays.py | 51 +++++++++++++++++++++ 5 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 openfisca_core/types/__init__.py create mode 100644 openfisca_core/types/data_types/__init__.py create mode 100644 openfisca_core/types/data_types/arrays.py diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 4d67c910ec..6a90622147 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,9 +1,11 @@ -from typing import Any, Dict, Sequence +from typing import Any, Dict, Sequence, TypeVar import numpy from openfisca_core.types import ArrayLike, ArrayType +T = TypeVar("T") + def apply_thresholds( input: ArrayType[float], @@ -88,9 +90,9 @@ def concat(this: ArrayLike[str], that: ArrayLike[str]) -> ArrayType[str]: def switch( - conditions: ArrayType[float], - value_by_condition: Dict[float, Any], - ) -> ArrayType[float]: + conditions: ArrayType[Any], + value_by_condition: Dict[float, T], + ) -> ArrayType[T]: """Mimicks a switch statement. Given an array of conditions, returns an array of the same size, @@ -101,7 +103,7 @@ def switch( value_by_condition: Values to replace for each condition. Returns: - :obj:`numpy.ndarray` of :obj:`float`: + :obj:`numpy.ndarray`: An array with the replaced values. Raises: diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index d7d07ea1d8..d682824207 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -102,7 +102,7 @@ def marginal_rate( marginal_rate = ( + 1 - - (target[:-1] - target[1:]) + - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) ) diff --git a/openfisca_core/types/__init__.py b/openfisca_core/types/__init__.py new file mode 100644 index 0000000000..e14cfea65d --- /dev/null +++ b/openfisca_core/types/__init__.py @@ -0,0 +1,45 @@ +"""Data types and protocols used by OpenFisca Core. + +The type definitions included in this sub-package are intented for +contributors, to help them better understand and document contracts +and expected behaviours. + +Official Public API: + * ``ArrayLike`` + * :attr:`.ArrayType` + +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_types import ( # noqa: F401 + ArrayLike, + ArrayType, + ) + +__all__ = ["ArrayLike", "ArrayType"] diff --git a/openfisca_core/types/data_types/__init__.py b/openfisca_core/types/data_types/__init__.py new file mode 100644 index 0000000000..6dd38194e3 --- /dev/null +++ b/openfisca_core/types/data_types/__init__.py @@ -0,0 +1 @@ +from .arrays import ArrayLike, ArrayType # noqa: F401 diff --git a/openfisca_core/types/data_types/arrays.py b/openfisca_core/types/data_types/arrays.py new file mode 100644 index 0000000000..5cfef639c5 --- /dev/null +++ b/openfisca_core/types/data_types/arrays.py @@ -0,0 +1,51 @@ +from typing import Sequence, TypeVar, Union + +from nptyping import types, NDArray as ArrayType + +import numpy + +T = TypeVar("T", bool, bytes, float, int, object, str) + +types._ndarray_meta._Type = Union[type, numpy.dtype, TypeVar] + +ArrayLike = Union[ArrayType[T], 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 + +""" From 4e07f30668256bf0363ff4cebe50e771818169f4 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 7 Oct 2021 14:23:06 +0200 Subject: [PATCH 09/11] Do not use slashes in .gitignore --- .gitignore | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 4b56efc6da..a110d33440 100644 --- a/.gitignore +++ b/.gitignore @@ -3,18 +3,18 @@ .spyderproject .pydevproject .vscode -.settings/ -.vscode/ -build/ -dist/ -doc/ +.settings +.vscode +build +dist +doc *.egg-info *.mo *.pyc *~ -/cover -/.coverage -/tags +cover +.coverage +tags .tags* .noseids .pytest_cache From 13c954e3b7f2b9ad710c7ea16c09c86a79f3949c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 7 Oct 2021 16:54:24 +0200 Subject: [PATCH 10/11] Sort .gitignore --- .gitignore | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index a110d33440..c66d2bd194 100644 --- a/.gitignore +++ b/.gitignore @@ -1,22 +1,22 @@ -.venv -.project -.spyderproject -.pydevproject -.vscode -.settings -.vscode -build -dist -doc *.egg-info *.mo *.pyc *~ -cover .coverage -tags -.tags* +.mypy_cache .noseids +.project +.pydevproject .pytest_cache -.mypy_cache +.settings +.spyderproject +.tags* +.venv +.vscode +.vscode +build +cover +dist +doc performance.json +tags From def6bf0ee3d8fcbfa54f9d0b786b6c4b5b6d7381 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 15:17:08 +0200 Subject: [PATCH 11/11] Bump minor to 35.6.0 --- CHANGELOG.md | 20 ++++++++++++++++++++ setup.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 153dd51231..684c94323a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 35.6.0 [#1054](https://github.com/openfisca/openfisca-core/pull/1054) + +#### New Features + +- Introduce `openfisca_core.types` + +#### Documentation + +- Complete typing of the commons module + +#### Dependencies + +- `nptyping` + - To add backport-support for numpy typing + - Can be removed once lower-bound numpy version is 1.21+ + +- `typing_extensions` + - To add backport-support for `typing.Protocol` and `typing.Literal` + - Can be removed once lower-bound python version is 3.8+ + ### 35.5.5 [#1055](https://github.com/openfisca/openfisca-core/pull/1055) #### Documentation diff --git a/setup.py b/setup.py index 151f899e8c..fb03815a1d 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup( name = 'OpenFisca-Core', - version = '35.5.5', + version = '35.6.0', author = 'OpenFisca Team', author_email = 'contact@openfisca.org', classifiers = [