From 307201ad83d6c38c67991188c8584f96d63c60a7 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:28:46 +0200 Subject: [PATCH 01/37] Sort deps alphabetically --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 1935635464..05da35a246 100644 --- a/setup.py +++ b/setup.py @@ -7,19 +7,19 @@ general_requirements = [ 'dpath >= 1.5.0, < 2.0.0', - 'pytest >= 4.4.1, < 6.0.0', # For openfisca test + 'numexpr >= 2.7.0, <= 3.0', 'numpy >= 1.11, < 1.21', 'psutil >= 5.4.7, < 6.0.0', + 'pytest >= 4.4.1, < 6.0.0', # For openfisca test 'PyYAML >= 3.10', 'sortedcontainers == 2.2.2', - 'numexpr >= 2.7.0, <= 3.0', ] api_requirements = [ - 'werkzeug >= 1.0.0, < 2.0.0', 'flask == 1.1.2', 'flask-cors == 3.0.10', 'gunicorn >= 20.0.0, < 21.0.0', + 'werkzeug >= 1.0.0, < 2.0.0', ] dev_requirements = [ @@ -28,10 +28,10 @@ 'flake8-bugbear >= 19.3.0, < 20.0.0', 'flake8-print >= 3.1.0, < 4.0.0', 'flake8-rst-docstrings < 1.0.0', - 'pytest-cov >= 2.6.1, < 3.0.0', 'mypy >= 0.701, < 0.800', 'openfisca-country-template >= 3.10.0, < 4.0.0', - 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0' + 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0', + 'pytest-cov >= 2.6.1, < 3.0.0', ] + api_requirements setup( From c14fd44c1db5f710849e7d8fb0dfcf43f87ea4b2 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:34:23 +0200 Subject: [PATCH 02/37] Add nptying for numpy type-checks --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 05da35a246..4d10109413 100644 --- a/setup.py +++ b/setup.py @@ -7,6 +7,7 @@ general_requirements = [ 'dpath >= 1.5.0, < 2.0.0', + 'nptyping >= 1.4.3, < 2.0.0', 'numexpr >= 2.7.0, <= 3.0', 'numpy >= 1.11, < 1.21', 'psutil >= 5.4.7, < 6.0.0', From f4638afd5d2d68dfc0e2ceb2f0105cc4d1be324a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:38:51 +0200 Subject: [PATCH 03/37] Add the types subpackage --- openfisca_core/types/__init__.py | 46 +++++++++++++++++ openfisca_core/types/data_types/__init__.py | 1 + openfisca_core/types/data_types/arrays.py | 56 +++++++++++++++++++++ 3 files changed, 103 insertions(+) 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/types/__init__.py b/openfisca_core/types/__init__.py new file mode 100644 index 0000000000..36dabd1899 --- /dev/null +++ b/openfisca_core/types/__init__.py @@ -0,0 +1,46 @@ +"""Data types and protocols used by OpenFisca Core. + +The type definitions included in this sub-package are intented mostly for +contributors, to help them better document contracts and 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, + ) + +#: Official Public API + +__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..3e941b4ab1 --- /dev/null +++ b/openfisca_core/types/data_types/arrays.py @@ -0,0 +1,56 @@ +from typing import Sequence, TypeVar, Union + +from nptyping import NDArray as ArrayType + +T = TypeVar("T", bool, bytes, float, int, object, str) + +A = Union[ + ArrayType[bool], + ArrayType[bytes], + ArrayType[float], + ArrayType[int], + ArrayType[object], + ArrayType[str], + ] + +ArrayLike = Union[A, 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 dcf0504f2141bda3a12454892e896f97b823a0e5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:46:21 +0200 Subject: [PATCH 04/37] Add darglint for docstring lints --- setup.cfg | 18 ++++++++++-------- setup.py | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4f98591eeb..0307bfa59c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,16 +5,18 @@ ; W503/504: We break lines before binary operators (Knuth's style) [flake8] -hang-closing = true -ignore = E128,E251,F403,F405,E501,W503,W504 -in-place = true -rst-roles = any, class, exc, meth, obj -rst-directives = attribute +hang-closing = true +extend-ignore = D +ignore = E128,E251,F403,F405,E501,W503,W504 +in-place = true +rst-directives = attribute +rst-roles = any, class, exc, meth, obj +strictness = short [tool:pytest] -addopts = --showlocals --doctest-modules --disable-pytest-warnings -testpaths = tests -python_files = **/*.py +addopts = --showlocals --doctest-modules --disable-pytest-warnings +testpaths = tests +python_files = **/*.py [mypy] ignore_missing_imports = True diff --git a/setup.py b/setup.py index 4d10109413..077e733b14 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,7 @@ dev_requirements = [ 'autopep8 >= 1.4.0, < 1.6.0', + 'darglint == 1.8.0', 'flake8 >= 3.9.0, < 4.0.0', 'flake8-bugbear >= 19.3.0, < 20.0.0', 'flake8-print >= 3.1.0, < 4.0.0', From 342fda850d024f9bbcc49aacdfb97c582a99d9fc Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:49:22 +0200 Subject: [PATCH 05/37] Add flake8-docstrings for docstring lints --- setup.cfg | 4 ++-- setup.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 0307bfa59c..b7a1aa1c1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,8 +9,8 @@ hang-closing = true extend-ignore = D ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true -rst-directives = attribute -rst-roles = any, class, exc, meth, obj +rst-directives = attribute, deprecated, seealso, versionadded, versionchanged +rst-roles = any, attr, class, exc, func, meth, obj strictness = short [tool:pytest] diff --git a/setup.py b/setup.py index 077e733b14..5086afb0f1 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ 'darglint == 1.8.0', 'flake8 >= 3.9.0, < 4.0.0', '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', 'mypy >= 0.701, < 0.800', From 596e05f51b4b18c889f1ee52fa3e2abef0e57388 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:50:34 +0200 Subject: [PATCH 06/37] Set flake8 jobs to automatic --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index b7a1aa1c1d..13d9bdc05f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,6 +9,7 @@ hang-closing = true extend-ignore = D ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true +jobs = 0 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, obj strictness = short From 28a2f49e0107afd14441b70573389619c6ac888a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:51:24 +0200 Subject: [PATCH 07/37] Add commons to flake8 doctest path --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 13d9bdc05f..2f29d2e405 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,6 +9,7 @@ hang-closing = true extend-ignore = D ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true +include-in-doctest = openfisca_core/commons jobs = 0 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, obj From 4a33d573662edd1b118ca5fe115437c05414b769 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:52:59 +0200 Subject: [PATCH 08/37] Add pylint for docstring lints --- setup.cfg | 5 +++++ setup.py | 1 + 2 files changed, 6 insertions(+) diff --git a/setup.cfg b/setup.cfg index 2f29d2e405..16ce15d85a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,6 +15,11 @@ rst-directives = attribute, deprecated, seealso, versionadded, versionchang rst-roles = any, attr, class, exc, func, meth, obj strictness = short +[pylint.message_control] +disable = all +jobs = 0 +score = no + [tool:pytest] addopts = --showlocals --doctest-modules --disable-pytest-warnings testpaths = tests diff --git a/setup.py b/setup.py index 5086afb0f1..dc55e3e783 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ 'flake8-print >= 3.1.0, < 4.0.0', 'flake8-rst-docstrings < 1.0.0', 'mypy >= 0.701, < 0.800', + 'pylint == 2.10.2', 'openfisca-country-template >= 3.10.0, < 4.0.0', 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0', 'pytest-cov >= 2.6.1, < 3.0.0', From 12611134f2ef8f0becb0820e392886291b024c5a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:55:15 +0200 Subject: [PATCH 09/37] Add coverage threshold to pytest --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 16ce15d85a..e2c80252eb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ jobs = 0 score = no [tool:pytest] -addopts = --showlocals --doctest-modules --disable-pytest-warnings +addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.69 --doctest-modules --disable-pytest-warnings --showlocals testpaths = tests python_files = **/*.py From 23203ca75a0672e6111b60f8c0d1b3640f47c277 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 22:58:12 +0200 Subject: [PATCH 10/37] Add doctests setup to pytest --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e2c80252eb..7c8a105c68 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,8 +22,9 @@ score = no [tool:pytest] addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.69 --doctest-modules --disable-pytest-warnings --showlocals -testpaths = tests +doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py +testpaths = tests [mypy] ignore_missing_imports = True From 8f322163d5df191ed16d96e652cd632d7dee1a02 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 23:00:15 +0200 Subject: [PATCH 11/37] Add types to tests path --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 7c8a105c68..8a5dac766e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,7 @@ hang-closing = true extend-ignore = D ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true -include-in-doctest = openfisca_core/commons +include-in-doctest = openfisca_core/commons openfisca_core/types jobs = 0 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, obj @@ -24,7 +24,7 @@ score = no addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.69 --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py -testpaths = tests +testpaths = openfisca_core/commons openfisca_core/types tests [mypy] ignore_missing_imports = True From 727344d622a9ec53091ee536d4a8dd9865b1e1cf Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 19 Sep 2021 23:06:04 +0200 Subject: [PATCH 12/37] Update mypy to avoid false-positives --- setup.cfg | 3 +++ setup.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 8a5dac766e..880fc8d31c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,5 +29,8 @@ testpaths = openfisca_core/commons openfisca_core/types tests [mypy] ignore_missing_imports = True +[mypy-openfisca_core.commons.tests.*] +ignore_errors = True + [mypy-openfisca_core.scripts.*] ignore_errors = True diff --git a/setup.py b/setup.py index dc55e3e783..432205cfd9 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.0.0.0, < 4.0.0.0', ] api_requirements = [ @@ -31,11 +32,13 @@ 'flake8-docstrings == 1.6.0', 'flake8-print >= 3.1.0, < 4.0.0', 'flake8-rst-docstrings < 1.0.0', - 'mypy >= 0.701, < 0.800', + 'mypy == 0.910', 'pylint == 2.10.2', 'openfisca-country-template >= 3.10.0, < 4.0.0', 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0', 'pytest-cov >= 2.6.1, < 3.0.0', + 'types-PyYAML == 5.4.10', + 'types-setuptools == 57.0.2', ] + api_requirements setup( From e9c07c9e6e242bc11e16e412a6618ec4bef2488a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 20 Sep 2021 00:08:14 +0200 Subject: [PATCH 13/37] Fix bug in make test-doc --- openfisca_tasks/lint.mk | 30 +++++++++++++++++++++++++++--- openfisca_tasks/test_code.mk | 2 +- openfisca_tasks/test_doc.mk | 3 +++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 8be8dd71ce..2e52457926 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -1,5 +1,5 @@ ## Lint the codebase. -lint: check-syntax-errors check-style check-types +lint: check-syntax-errors check-style lint-styling-doc check-types @$(call print_pass,$@:) ## Compile python files to check for syntax errors. @@ -14,10 +14,34 @@ check-style: $(shell git ls-files "*.py") @flake8 $? @$(call print_pass,$@:) +## Run linters to check for syntax and style errors in the doc. +lint-styling-doc: \ + lint-styling-doc-commons \ + lint-styling-doc-types \ + ; + +## Run linters to check for syntax and style errors in the doc. +lint-styling-doc-%: + @## These checks are exclusively related to doc/strings/test. + @## + @## They can be integrated into setup.cfg once all checks pass. + @## The reason they're here is because otherwise we wouldn't be + @## able to integrate documentation improvements progresively. + @## + @## D101: Each class has to have at least one doctest. + @## D102: Each public method has to have at least one doctest. + @## D103: Each public function has to have at least one doctest. + @## DARXXX: https://github.com/terrencepreilly/darglint#error-codes. + @## + @$(call print_help,$(subst $*,%,$@:)) + @flake8 --select=D101,D102,D103,DAR openfisca_core/$* + @pylint openfisca_core/$* + @$(call print_pass,$@:) + ## Run static type checkers for type errors. -check-types: openfisca_core openfisca_web_api +check-types: @$(call print_help,$@:) - @mypy $? + @mypy --package openfisca_core --package openfisca_web_api @$(call print_pass,$@:) ## Run code formatters to correct style errors. diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index 8a24ad69b1..ffa87efbc4 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -19,7 +19,7 @@ test-code: test-core test-country test-extension ## Run openfisca-core tests. test-core: $(shell git ls-files "tests/*.py") @$(call print_help,$@:) - @PYTEST_ADDOPTS="${PYTEST_ADDOPTS} --cov=openfisca_core ${pytest_args}" \ + @PYTEST_ADDOPTS="${PYTEST_ADDOPTS} --cov=openfisca_core --cov=openfisca_web_api ${pytest_args}" \ openfisca test $? \ ${openfisca_args} @$(call print_pass,$@:) diff --git a/openfisca_tasks/test_doc.mk b/openfisca_tasks/test_doc.mk index fb7249c99e..bce952fe81 100644 --- a/openfisca_tasks/test_doc.mk +++ b/openfisca_tasks/test_doc.mk @@ -62,14 +62,17 @@ test-doc-checkout: } \ || git pull --ff-only origin master ; \ } 1> /dev/null + @$(call print_pass,$@:) ## Install doc dependencies. test-doc-install: @$(call print_help,$@:) @pip install --requirement doc/requirements.txt 1> /dev/null @pip install --editable .[dev] --upgrade 1> /dev/null + @$(call print_pass,$@:) ## Dry-build the doc. test-doc-build: @$(call print_help,$@:) @sphinx-build -M dummy doc/source doc/build -n -q -W + @$(call print_pass,$@:) From 39aa617803b7e06f436840c4a5513f5f2d17cbba Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 20 Sep 2021 00:25:21 +0200 Subject: [PATCH 14/37] Add commons/decorators --- openfisca_core/commons/__init__.py | 1 + openfisca_core/commons/decorators.py | 76 +++++++++++++++++++ openfisca_core/commons/tests/__init__.py | 0 .../commons/tests/test_decorators.py | 20 +++++ 4 files changed, 97 insertions(+) create mode 100644 openfisca_core/commons/decorators.py create mode 100644 openfisca_core/commons/tests/__init__.py create mode 100644 openfisca_core/commons/tests/test_decorators.py diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index db41ed1874..42c1ada223 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -23,6 +23,7 @@ from .dummy import Dummy # noqa: F401 +from .decorators import deprecated # noqa: F401 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 diff --git a/openfisca_core/commons/decorators.py b/openfisca_core/commons/decorators.py new file mode 100644 index 0000000000..2041a7f4f7 --- /dev/null +++ b/openfisca_core/commons/decorators.py @@ -0,0 +1,76 @@ +import functools +import warnings +import typing +from typing import Any, Callable, TypeVar + +T = Callable[..., Any] +F = TypeVar("F", bound = T) + + +class deprecated: + """Allows (soft) deprecating a functionality of OpenFisca. + + Attributes: + since (:obj:`str`): Since when the functionality is deprecated. + expires (:obj:`str`): When will it be removed forever? + + Args: + since: Since when the functionality is deprecated. + expires: When will it be removed forever? + + Examples: + >>> @deprecated(since = "35.5.0", expires = "in the future") + ... def obsolete(): + ... return "I'm obsolete!" + + >>> repr(obsolete) + '' + + >>> str(obsolete) + '' + + .. versionadded:: 35.6.0 + + """ + + since: str + expires: str + + def __init__(self, since: str, expires: str) -> None: + self.since = since + self.expires = expires + + def __call__(self, function: F) -> F: + """Wraps a function to return another one, decorated. + + Args: + function: The function or method to decorate. + + Returns: + :obj:`callable`: The decorated function. + + Examples: + >>> def obsolete(): + ... return "I'm obsolete!" + + >>> decorator = deprecated( + ... since = "35.5.0", + ... expires = "in the future", + ... ) + + >>> decorator(obsolete) + + + """ + + def wrapper(*args: Any, **kwds: Any) -> Any: + message = [ + f"{function.__qualname__} has been deprecated since", + f"version {self.since}, and will be removed in", + f"{self.expires}.", + ] + warnings.warn(" ".join(message), DeprecationWarning) + return function(*args, **kwds) + + functools.update_wrapper(wrapper, function) + return typing.cast(F, wrapper) diff --git a/openfisca_core/commons/tests/__init__.py b/openfisca_core/commons/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/commons/tests/test_decorators.py b/openfisca_core/commons/tests/test_decorators.py new file mode 100644 index 0000000000..04c5ce3d91 --- /dev/null +++ b/openfisca_core/commons/tests/test_decorators.py @@ -0,0 +1,20 @@ +import re + +import pytest + +from openfisca_core.commons import deprecated + + +def test_deprecated(): + """The decorated function throws a deprecation warning when used.""" + + since = "yesterday" + expires = "doomsday" + match = re.compile(f"^.*{since}.*{expires}.*$") + + @deprecated(since, expires) + def function(a: int, b: float) -> float: + return a + b + + with pytest.warns(DeprecationWarning, match = match): + assert function(1, 2.) == 3. From 757c0087b7c9272bf69e08bcd369e9d293809b7b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 20 Sep 2021 00:27:40 +0200 Subject: [PATCH 15/37] Fix commons/dummy doctests --- openfisca_core/commons/dummy.py | 21 +++++++++++++-------- openfisca_core/commons/tests/test_dummy.py | 10 ++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 openfisca_core/commons/tests/test_dummy.py diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 4136a0d429..732ed49a65 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -1,13 +1,18 @@ -import warnings +from .decorators import deprecated class Dummy: - """A class that does nothing.""" + """A class that did nothing. + Examples: + >>> Dummy() + None: - message = [ - "The 'Dummy' class has been deprecated since version 34.7.0,", - "and will be removed in the future.", - ] - warnings.warn(" ".join(message), DeprecationWarning) - pass + ... diff --git a/openfisca_core/commons/tests/test_dummy.py b/openfisca_core/commons/tests/test_dummy.py new file mode 100644 index 0000000000..d4ecec3842 --- /dev/null +++ b/openfisca_core/commons/tests/test_dummy.py @@ -0,0 +1,10 @@ +import pytest + +from openfisca_core.commons import Dummy + + +def test_dummy_deprecation(): + """Dummy throws a deprecation warning when instantiated.""" + + with pytest.warns(DeprecationWarning): + assert Dummy() From a75f83685bb67d595fc858610a6bd8ebd4ffd1f8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 20 Sep 2021 00:28:42 +0200 Subject: [PATCH 16/37] Fix commons/formulas doctests --- openfisca_core/commons/formulas.py | 127 ++++++++++++++---- openfisca_core/commons/tests/test_formulas.py | 81 +++++++++++ 2 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 openfisca_core/commons/tests/test_formulas.py diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 4fadc1b518..a750717f1d 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,52 +1,125 @@ +from typing import Any, Dict, List + import numpy +from openfisca_core.types import ArrayLike, ArrayType + + +def apply_thresholds( + input: ArrayType[float], + thresholds: ArrayLike[float], + choices: ArrayLike[float], + ) -> ArrayType[float]: + """Makes a choice based on an input and thresholds. + + From list of ``choices``, it selects one of them based on a list of + inputs, depending on the position of each ``input`` whithin a list of + ``thresholds``. It does so for each ``input`` provided. + + Args: + input: A list of inputs to make a choice. + thresholds: A list of thresholds to choose. + choices: A list of the possible choices. + + Returns: + :obj:`numpy.ndarray` of :obj:`float`: + A list of the choices made. + + Raises: + :exc:`AssertionError`: When the number of ``thresholds`` (t) and the + number of choices (c) are not either t == c or t == c - 1. + + Examples: + >>> input = numpy.array([4, 5, 6, 7, 8]) + >>> thresholds = [5, 7] + >>> choices = [10, 15, 20] + >>> apply_thresholds(input, thresholds, choices) + array([10, 10, 15, 15, 20]) -def apply_thresholds(input, thresholds, choices): - """ - Return one of the choices depending on the input position compared to thresholds, for each input. - - >>> apply_thresholds(np.array([4]), [5, 7], [10, 15, 20]) - array([10]) - >>> apply_thresholds(np.array([5]), [5, 7], [10, 15, 20]) - array([10]) - >>> apply_thresholds(np.array([6]), [5, 7], [10, 15, 20]) - array([15]) - >>> apply_thresholds(np.array([8]), [5, 7], [10, 15, 20]) - array([20]) - >>> apply_thresholds(np.array([10]), [5, 7, 9], [10, 15, 20]) - array([0]) """ + + condlist: List[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): - if isinstance(this, numpy.ndarray) and not numpy.issubdtype(this.dtype, numpy.str): +def concat(this: ArrayLike[str], that: ArrayLike[str]) -> ArrayType[str]: + """Concatenates the values of two arrays. + + Args: + this: An array to concatenate. + that: Another array to concatenate. + + Returns: + :obj:`numpy.ndarray` of :obj:`float`: + An array with the concatenated values. + + Examples: + >>> this = ["this", "that"] + >>> that = numpy.array([1, 2.5]) + >>> concat(this, that) + array(['this1.0', 'that2.5']...) + + """ + + 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: ArrayType[float], + value_by_condition: Dict[float, Any], + ) -> ArrayType[float]: + """Reproduces a switch statement. + Given an array of conditions, returns an array of the same size, + replacing each condition item by the corresponding given value. -def switch(conditions, value_by_condition): - ''' - Reproduces a switch statement: given an array of conditions, return an array of the same size replacing each - condition item by the corresponding given value. + Args: + conditions: An array of conditions. + value_by_condition: Values to replace for each condition. - Example: - >>> switch(np.array([1, 1, 1, 2]), {1: 80, 2: 90}) + Returns: + :obj:`numpy.ndarray` of :obj:`float`: + An array with the replaced values. + + Raises: + :exc:`AssertionError`: When ``value_by_condition`` is empty. + + Examples: + >>> conditions = numpy.array([1, 1, 1, 2]) + >>> value_by_condition = {1: 80, 2: 90} + >>> switch(conditions, value_by_condition) array([80, 80, 80, 90]) - ''' + + """ + assert len(value_by_condition) > 0, \ "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()) diff --git a/openfisca_core/commons/tests/test_formulas.py b/openfisca_core/commons/tests/test_formulas.py new file mode 100644 index 0000000000..f05725cb80 --- /dev/null +++ b/openfisca_core/commons/tests/test_formulas.py @@ -0,0 +1,81 @@ +import numpy +import pytest +from numpy.testing import assert_array_equal + +from openfisca_core import commons + + +def test_apply_thresholds_when_several_inputs(): + """Makes a choice for any given input.""" + + input_ = numpy.array([4, 5, 6, 7, 8, 9, 10]) + thresholds = [5, 7, 9] + choices = [10, 15, 20, 25] + + result = commons.apply_thresholds(input_, thresholds, choices) + + assert_array_equal(result, [10, 10, 15, 15, 20, 20, 25]) + + +def test_apply_thresholds_when_too_many_thresholds(): + """Raises an AssertionError when thresholds > choices.""" + + input_ = numpy.array([6]) + thresholds = [5, 7, 9, 11] + choices = [10, 15, 20] + + with pytest.raises(AssertionError): + assert commons.apply_thresholds(input_, thresholds, choices) + + +def test_apply_thresholds_when_too_many_choices(): + """Raises an AssertionError when thresholds < choices - 1.""" + + input_ = numpy.array([6]) + thresholds = [5, 7] + choices = [10, 15, 20, 25] + + with pytest.raises(AssertionError): + assert commons.apply_thresholds(input_, thresholds, choices) + + +def test_concat_when_this_is_array_not_str(): + """Casts ``this`` to ``str`` when it is a numpy array other than string.""" + + this = numpy.array([1, 2]) + that = numpy.array(["la", "o"]) + + result = commons.concat(this, that) + + assert_array_equal(result, ["1la", "2o"]) + + +def test_concat_when_that_is_array_not_str(): + """Casts ``that`` to ``str`` when it is a numpy array other than string.""" + + this = numpy.array(["ho", "cha"]) + that = numpy.array([1, 2]) + + result = commons.concat(this, that) + + assert_array_equal(result, ["ho1", "cha2"]) + + +def test_concat_when_args_not_str_array_like(): + """Raises a TypeError when args are not a string array-like object.""" + + this = (1, 2) + that = (3, 4) + + with pytest.raises(TypeError): + commons.concat(this, that) + + +def test_switch_when_values_are_empty(): + """Raises an AssertionError when the values are empty.""" + + conditions = [1, 1, 1, 2] + value_by_condition = {} + + with pytest.raises(AssertionError): + assert commons.switch(conditions, value_by_condition) From cc8b623e94d1960d2002d2df6879bd2d25282153 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 20 Sep 2021 00:29:32 +0200 Subject: [PATCH 17/37] Fix commons/rates doctests --- openfisca_core/commons/rates.py | 125 ++++++++++++++++++--- openfisca_core/commons/tests/test_rates.py | 26 +++++ 2 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 openfisca_core/commons/tests/test_rates.py diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index 4bc8580193..487abd3aea 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -1,27 +1,124 @@ +from typing import Optional + import numpy +from openfisca_core.types import ArrayLike, ArrayType + + +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 + income. Optionally, a ``trim`` can be applied consisting on the lower and + upper bounds of the average rate to be computed. + + Note: + Usually, ``target`` and ``varying`` are the same size. + + Args: + target: The targeted net income. + varying: The varying gross income. + trim: The lower and upper bounds of the average rate. + + Returns: + :obj:`numpy.ndarray` of :obj:`float`: -def average_rate(target = None, varying = None, trim = None): - ''' - Computes the average rate of a targeted net income, according to the varying gross income. + The average rate for each target. + + When ``trim`` is provided, values that are out of the provided bounds + are replaced by :obj:`numpy.nan`. + + Examples: + >>> target = numpy.array([1, 2, 3]) + >>> varying = [2, 2, 2] + >>> trim = [-1, .25] + >>> average_rate(target, varying, trim) + array([ nan, 0. , -0.5]) + + """ + + average_rate: ArrayType[float] - :param target: Targeted net income, numerator - :param varying: Varying gross income, denominator - :param trim: Lower and upper bound of average rate to return - ''' 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): - # target: numerator, varying: denominator - marginal_rate = 1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) +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 + income. Optionally, a ``trim`` can be applied consisting of the lower and + upper bounds of the marginal rate to be computed. + + Note: + Usually, ``target`` and ``varying`` are the same size. + + Args: + target: The targeted net income. + varying: The varying gross income. + trim: The lower and upper bounds of the marginal rate. + + Returns: + :obj:`numpy.ndarray` of :obj:`float`: + + The marginal rate for each target. + + When ``trim`` is provided, values that are out of the provided bounds + are replaced by :obj:`numpy.nan`. + + Examples: + >>> target = numpy.array([1, 2, 3]) + >>> varying = numpy.array([1, 2, 4]) + >>> trim = [.25, .75] + >>> marginal_rate(target, varying, trim) + array([nan, 0.5]) + + """ + + 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 diff --git a/openfisca_core/commons/tests/test_rates.py b/openfisca_core/commons/tests/test_rates.py new file mode 100644 index 0000000000..e603a05241 --- /dev/null +++ b/openfisca_core/commons/tests/test_rates.py @@ -0,0 +1,26 @@ +import numpy +from numpy.testing import assert_array_equal + +from openfisca_core import commons + + +def test_average_rate_when_varying_is_zero(): + """Yields infinity when the varying gross income crosses zero.""" + + target = numpy.array([1, 2, 3]) + varying = [0, 0, 0] + + result = commons.average_rate(target, varying) + + assert_array_equal(result, [- numpy.inf, - numpy.inf, - numpy.inf]) + + +def test_marginal_rate_when_varying_is_zero(): + """Yields infinity when the varying gross income crosses zero.""" + + target = numpy.array([1, 2, 3]) + varying = numpy.array([0, 0, 0]) + + result = commons.marginal_rate(target, varying) + + assert_array_equal(result, [numpy.inf, numpy.inf]) From 54a8706ee4dfc3af7d9b7ccf3d413d7f5faf5300 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 20 Sep 2021 00:30:27 +0200 Subject: [PATCH 18/37] Fix commons/misc doctests --- openfisca_core/commons/misc.py | 74 +++++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index eb2bc7372c..5056389c6e 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,20 +1,76 @@ -import numpy +from typing import TypeVar +from openfisca_core.types import ArrayType -def empty_clone(original): - """Create a new empty instance of the same class of the original object.""" - class Dummy(original.__class__): - def __init__(self) -> None: - pass +T = TypeVar("T") + + +def empty_clone(original: T) -> T: + """Creates an empty instance of the same class of the original object. + + Args: + original: An object to clone. + + Returns: + The cloned, empty, object. + + Examples: + >>> Foo = type("Foo", (list,), {}) + >>> foo = Foo([1, 2, 3]) + >>> foo + [1, 2, 3] + + >>> bar = empty_clone(foo) + >>> bar + [] + + >>> isinstance(bar, Foo) + True + + """ + + Dummy: object + new: T + + Dummy = type( + "Dummy", + (original.__class__,), + {"__init__": lambda self: None}, + ) new = Dummy() new.__class__ = original.__class__ return new -def stringify_array(array: numpy.ndarray) -> str: - """ - Generate a clean string representation of a NumPY array. +def stringify_array(array: ArrayType) -> str: + """Generates a clean string representation of a numpy array. + + Args: + array: An array. + + Returns: + :obj:`str`: + "None" if the ``array`` is None, the stringified ``array`` otherwise. + + Examples: + >>> import numpy + + >>> stringify_array(None) + 'None' + + >>> array = numpy.array([10, 20.]) + >>> stringify_array(array) + '[10.0, 20.0]' + + >>> array = numpy.array(["10", "Twenty"]) + >>> stringify_array(array) + '[10, Twenty]' + + >>> array = numpy.array([list, dict(), stringify_array]) + >>> stringify_array(array) + "[, {}, Date: Mon, 20 Sep 2021 00:32:51 +0200 Subject: [PATCH 19/37] Add doc to commons module --- openfisca_core/commons/__init__.py | 88 ++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index 42c1ada223..eec40ddec4 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -1,29 +1,71 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from openfisca_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from openfisca_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from openfisca_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports +"""Common tools for contributors and users. -from .dummy import Dummy # noqa: F401 +The tools included in this sub-package are intented, at the same time, to help +contributors who maintain OpenFisca Core, and to help users building their own +systems. + +Official Public API: + * :class:`.deprecated` + * :func:`.apply_thresholds` + * :func:`.average_rate` + * :func:`.concat` + * :func:`.empty_clone` + * :func:`.marginal_rate` + * :func:`.stringify_array` + * :func:`.switch` + +Deprecated: + * :class:`.Dummy` + +Note: + The ``deprecated`` imports are transitional, as so to ensure non-breaking + changes, and could be definitely removed from the codebase in the next + major release. + +Note: + How imports are being used today:: + + from openfisca_core.commons import * # Bad + from openfisca_core.commons.formulas import switch # Bad + from openfisca_core.commons.decorators import deprecated # 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 import commons + from openfisca_core.commons import deprecated + + deprecated() # Good: import classes as publicly exposed + commons.switch() # Good: use functions 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 .decorators import deprecated # noqa: F401 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 + +__all__ = ["deprecated"] +__all__ = ["apply_thresholds", "concat", "switch", *__all__] +__all__ = ["empty_clone", "stringify_array", *__all__] +__all__ = ["average_rate", "marginal_rate", *__all__] + +# Deprecated + +from .dummy import Dummy # noqa: F401 + +__all__ = ["Dummy", *__all__] From 6578561fda87c630f910a5461cd45b4830dc0d0d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 20 Sep 2021 00:35:45 +0200 Subject: [PATCH 20/37] Delete redundant tests --- tests/core/test_commons.py | 36 ----------------------- tests/core/test_formula_helpers.py | 47 ------------------------------ tests/core/test_rates.py | 20 ------------- 3 files changed, 103 deletions(-) delete mode 100644 tests/core/test_commons.py delete mode 100644 tests/core/test_formula_helpers.py delete mode 100644 tests/core/test_rates.py diff --git a/tests/core/test_commons.py b/tests/core/test_commons.py deleted file mode 100644 index ddbf30e5a9..0000000000 --- a/tests/core/test_commons.py +++ /dev/null @@ -1,36 +0,0 @@ -import numpy - -from openfisca_core import commons - -import pytest - - -def test_dummy(): - with pytest.warns(DeprecationWarning): - result = commons.Dummy() - assert result - - -def test_empty_clone(): - dummy_class = type("Dummmy", (), {}) - dummy = dummy_class() - - result = commons.empty_clone(dummy) - - assert type(result) == dummy_class - - -def test_stringify_array(): - array = numpy.array([10, 20]) - - result = commons.stringify_array(array) - - assert result == "[10, 20]" - - -def test_stringify_array_when_none(): - array = None - - result = commons.stringify_array(array) - - assert result == "None" diff --git a/tests/core/test_formula_helpers.py b/tests/core/test_formula_helpers.py deleted file mode 100644 index 51bc2a2e20..0000000000 --- a/tests/core/test_formula_helpers.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy -import pytest - -from openfisca_core.formula_helpers import apply_thresholds as apply_thresholds -from openfisca_core.tools import assert_near - - -def test_apply_thresholds_with_too_many_thresholds(): - input = numpy.array([10]) - thresholds = [5, 4] - choice_list = [10] - with pytest.raises(AssertionError): - return apply_thresholds(input, thresholds, choice_list) - - -def test_apply_thresholds_with_too_few_thresholds(): - input = numpy.array([10]) - thresholds = [5] - choice_list = [10, 15, 20] - with pytest.raises(AssertionError): - return apply_thresholds(input, thresholds, choice_list) - - -def test_apply_thresholds(): - input = numpy.array([4, 5, 6, 7, 8]) - thresholds = [5, 7] - choice_list = [10, 15, 20] - result = apply_thresholds(input, thresholds, choice_list) - assert_near(result, [10, 10, 15, 15, 20]) - - -def test_apply_thresholds_with_as_many_thresholds_than_choices(): - input = numpy.array([4, 6, 8]) - thresholds = [5, 7] - choice_list = [10, 20] - result = apply_thresholds(input, thresholds, choice_list) - assert_near(result, [10, 20, 0]) - - -def test_apply_thresholds_with_variable_threshold(): - input = numpy.array([1000, 1000, 1000]) - thresholds = [numpy.array([500, 1500, 1000])] # Only one thresold, but varies with the person - choice_list = [True, False] # True if input <= threshold, false otherwise - result = apply_thresholds(input, thresholds, choice_list) - assert_near(result, [False, True, True]) diff --git a/tests/core/test_rates.py b/tests/core/test_rates.py deleted file mode 100644 index 8ab2170954..0000000000 --- a/tests/core/test_rates.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy - -from openfisca_core.rates import average_rate - - -def test_average_rate(): - '''Compute the average tax rate when the gross income is never zero''' - target = numpy.array([1, 2, 3]) - result = average_rate(target, varying = 2) - expected = numpy.array([.5, 0, -.5]) - numpy.testing.assert_equal(result, expected) - - -def test_average_rate_when_varying_is_zero(): - '''Compute the average tax rate when the varying gross income cross zero (yields infinity)''' - target = numpy.array([1, 2, 3]) - result = average_rate(target, varying = 0) - assert numpy.isinf(result[0]).all() From 949188cb54b91df66f87c7b55b02e201ead187d7 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 23 Sep 2021 20:42:28 +0200 Subject: [PATCH 21/37] Improve wording in the commons module Co-authored-by: Matti Schneider --- openfisca_core/commons/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index eec40ddec4..10015e2785 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -30,11 +30,11 @@ from openfisca_core.commons.decorators import deprecated # Bad - The previous examples provoke cyclic dependency problems, that prevents us - from modularizing the different components of the library, so as to make + The previous examples provoke cyclic dependency problems, that prevent us + from modularizing the different components of the library, which would make them easier to test and to maintain. - How could them be used after the next major release:: + How they could be used in a future release: from openfisca_core import commons from openfisca_core.commons import deprecated From e96dd4640417b712be38a5e8e16cdf5c3f227b33 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 24 Sep 2021 19:38:05 +0200 Subject: [PATCH 22/37] Remove deprecation expiration --- openfisca_core/commons/dummy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 732ed49a65..7cd1951931 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -9,10 +9,11 @@ class Dummy: None: ... From 6eb0bdf83a8eec295156bfddc204e05468528f1e Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 27 Sep 2021 15:11:15 +0200 Subject: [PATCH 23/37] Remove decorators --- openfisca_core/commons/__init__.py | 5 +- openfisca_core/commons/decorators.py | 76 ------------------- openfisca_core/commons/dummy.py | 9 ++- .../commons/tests/test_decorators.py | 20 ----- 4 files changed, 8 insertions(+), 102 deletions(-) delete mode 100644 openfisca_core/commons/decorators.py delete mode 100644 openfisca_core/commons/tests/test_decorators.py diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index 10015e2785..fa42930db7 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -5,7 +5,6 @@ systems. Official Public API: - * :class:`.deprecated` * :func:`.apply_thresholds` * :func:`.average_rate` * :func:`.concat` @@ -54,13 +53,11 @@ # Official Public API -from .decorators import deprecated # noqa: F401 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 -__all__ = ["deprecated"] -__all__ = ["apply_thresholds", "concat", "switch", *__all__] +__all__ = ["apply_thresholds", "concat", "switch"] __all__ = ["empty_clone", "stringify_array", *__all__] __all__ = ["average_rate", "marginal_rate", *__all__] diff --git a/openfisca_core/commons/decorators.py b/openfisca_core/commons/decorators.py deleted file mode 100644 index 2041a7f4f7..0000000000 --- a/openfisca_core/commons/decorators.py +++ /dev/null @@ -1,76 +0,0 @@ -import functools -import warnings -import typing -from typing import Any, Callable, TypeVar - -T = Callable[..., Any] -F = TypeVar("F", bound = T) - - -class deprecated: - """Allows (soft) deprecating a functionality of OpenFisca. - - Attributes: - since (:obj:`str`): Since when the functionality is deprecated. - expires (:obj:`str`): When will it be removed forever? - - Args: - since: Since when the functionality is deprecated. - expires: When will it be removed forever? - - Examples: - >>> @deprecated(since = "35.5.0", expires = "in the future") - ... def obsolete(): - ... return "I'm obsolete!" - - >>> repr(obsolete) - '' - - >>> str(obsolete) - '' - - .. versionadded:: 35.6.0 - - """ - - since: str - expires: str - - def __init__(self, since: str, expires: str) -> None: - self.since = since - self.expires = expires - - def __call__(self, function: F) -> F: - """Wraps a function to return another one, decorated. - - Args: - function: The function or method to decorate. - - Returns: - :obj:`callable`: The decorated function. - - Examples: - >>> def obsolete(): - ... return "I'm obsolete!" - - >>> decorator = deprecated( - ... since = "35.5.0", - ... expires = "in the future", - ... ) - - >>> decorator(obsolete) - - - """ - - def wrapper(*args: Any, **kwds: Any) -> Any: - message = [ - f"{function.__qualname__} has been deprecated since", - f"version {self.since}, and will be removed in", - f"{self.expires}.", - ] - warnings.warn(" ".join(message), DeprecationWarning) - return function(*args, **kwds) - - functools.update_wrapper(wrapper, function) - return typing.cast(F, wrapper) diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 7cd1951931..d1e9fbe814 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -1,4 +1,4 @@ -from .decorators import deprecated +import warnings class Dummy: @@ -14,6 +14,11 @@ class Dummy: """ - @deprecated(since = "34.7.0", expires = "in the future") def __init__(self) -> None: + message = [ + "The 'Dummy' class has been deprecated since version 34.7.0,", + "and will be removed in the future.", + ] + warnings.warn(" ".join(message), DeprecationWarning) + ... diff --git a/openfisca_core/commons/tests/test_decorators.py b/openfisca_core/commons/tests/test_decorators.py deleted file mode 100644 index 04c5ce3d91..0000000000 --- a/openfisca_core/commons/tests/test_decorators.py +++ /dev/null @@ -1,20 +0,0 @@ -import re - -import pytest - -from openfisca_core.commons import deprecated - - -def test_deprecated(): - """The decorated function throws a deprecation warning when used.""" - - since = "yesterday" - expires = "doomsday" - match = re.compile(f"^.*{since}.*{expires}.*$") - - @deprecated(since, expires) - def function(a: int, b: float) -> float: - return a + b - - with pytest.warns(DeprecationWarning, match = match): - assert function(1, 2.) == 3. From 37f1cdcce882c1f268346ca709eb36711cab9e55 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 02:58:08 +0200 Subject: [PATCH 24/37] Add doc checks to setup.cfg --- setup.cfg | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 880fc8d31c..502b3a86da 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,14 +10,13 @@ extend-ignore = D ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true include-in-doctest = openfisca_core/commons openfisca_core/types -jobs = 0 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, obj strictness = short [pylint.message_control] disable = all -jobs = 0 +enable = C0115,C0116,R0401 score = no [tool:pytest] From 7ce2ccceea1f8a4301cc45e50798dd7f11577c7c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 03:01:02 +0200 Subject: [PATCH 25/37] Adapt coverage config --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 502b3a86da..bdef34b16e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ enable = C0115,C0116,R0401 score = no [tool:pytest] -addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.69 --doctest-modules --disable-pytest-warnings --showlocals +addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.58 --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py testpaths = openfisca_core/commons openfisca_core/types tests From 7f15099c1c2f5c5c3844740c7c5c0f841064e6f6 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 03:16:17 +0200 Subject: [PATCH 26/37] Add explanation to checks in Makefile --- setup.cfg | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index bdef34b16e..0512b02b34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,8 @@ -; E128/133: We prefer hang-closing visual indents -; E251: We prefer `function(x = 1)` over `function(x=1)` -; E501: We do not enforce a maximum line length -; F403/405: We ignore * imports -; W503/504: We break lines before binary operators (Knuth's style) +; E128/133: We prefer hang-closing visual indents +; E251: We prefer `function(x = 1)` over `function(x=1)` +; E501: We do not enforce a maximum line length +; F403/405: We ignore * imports +; W503/504: We break lines before binary operators (Knuth's style) [flake8] hang-closing = true @@ -14,6 +14,10 @@ rst-directives = attribute, deprecated, seealso, versionadded, versionchang rst-roles = any, attr, class, exc, func, meth, obj strictness = short +; C0115: We document classes. +; C0116: We document public functions. +; R0401: We avoid cyclic imports —required for unit/doc tests. + [pylint.message_control] disable = all enable = C0115,C0116,R0401 From b215b30ad5b2f7edae6d80a7a1f61af691664a18 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 03:28:25 +0200 Subject: [PATCH 27/37] Remove unnecessary ellipsis --- openfisca_core/commons/dummy.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index d1e9fbe814..14515fd7e0 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -20,5 +20,3 @@ def __init__(self) -> None: "and will be removed in the future.", ] warnings.warn(" ".join(message), DeprecationWarning) - - ... From e2e1fe944913226bc8df2578587b6ea19e9576b4 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 16:02:01 +0200 Subject: [PATCH 28/37] Rollback commons module doc --- openfisca_core/commons/__init__.py | 86 ++++++++---------------------- 1 file changed, 23 insertions(+), 63 deletions(-) diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index fa42930db7..db41ed1874 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -1,68 +1,28 @@ -"""Common tools for contributors and users. +# Transitional imports to ensure non-breaking changes. +# Could be deprecated in the next major release. +# +# How imports are being used today: +# +# >>> from openfisca_core.module import symbol +# +# The previous example provokes cyclic dependency problems +# that prevent us from modularizing the different components +# of the library so to make them easier to test and to maintain. +# +# How could them be used after the next major release: +# +# >>> from openfisca_core import module +# >>> module.symbol() +# +# And for classes: +# +# >>> from openfisca_core.module import Symbol +# >>> Symbol() +# +# See: https://www.python.org/dev/peps/pep-0008/#imports -The tools included in this sub-package are intented, at the same time, to help -contributors who maintain OpenFisca Core, and to help users building their own -systems. - -Official Public API: - * :func:`.apply_thresholds` - * :func:`.average_rate` - * :func:`.concat` - * :func:`.empty_clone` - * :func:`.marginal_rate` - * :func:`.stringify_array` - * :func:`.switch` - -Deprecated: - * :class:`.Dummy` - -Note: - The ``deprecated`` imports are transitional, as so to ensure non-breaking - changes, and could be definitely removed from the codebase in the next - major release. - -Note: - How imports are being used today:: - - from openfisca_core.commons import * # Bad - from openfisca_core.commons.formulas import switch # Bad - from openfisca_core.commons.decorators import deprecated # Bad - - - The previous examples provoke cyclic dependency problems, that prevent us - from modularizing the different components of the library, which would make - them easier to test and to maintain. - - How they could be used in a future release: - - from openfisca_core import commons - from openfisca_core.commons import deprecated - - deprecated() # Good: import classes as publicly exposed - commons.switch() # Good: use functions 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 .dummy import Dummy # noqa: F401 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 - -__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__ = ["Dummy", *__all__] From 851b77044b1174ec9b46525d84d9f986b57d6c93 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 16:04:19 +0200 Subject: [PATCH 29/37] Rollback dummy doc --- openfisca_core/commons/dummy.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 14515fd7e0..4136a0d429 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -2,17 +2,7 @@ class Dummy: - """A class that did nothing. - - Examples: - >>> Dummy() - None: message = [ @@ -20,3 +10,4 @@ def __init__(self) -> None: "and will be removed in the future.", ] warnings.warn(" ".join(message), DeprecationWarning) + pass From 778655d0422bacb9430e315b1447af5559b24828 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 16:14:55 +0200 Subject: [PATCH 30/37] Rollback formulas doc --- openfisca_core/commons/formulas.py | 91 +++++------------------------- setup.cfg | 2 +- 2 files changed, 16 insertions(+), 77 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index a750717f1d..5b49387fde 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,33 +1,9 @@ -from typing import Any, Dict, List - import numpy -from openfisca_core.types import ArrayLike, ArrayType - - -def apply_thresholds( - input: ArrayType[float], - thresholds: ArrayLike[float], - choices: ArrayLike[float], - ) -> ArrayType[float]: - """Makes a choice based on an input and thresholds. - - From list of ``choices``, it selects one of them based on a list of - inputs, depending on the position of each ``input`` whithin a list of - ``thresholds``. It does so for each ``input`` provided. - - Args: - input: A list of inputs to make a choice. - thresholds: A list of thresholds to choose. - choices: A list of the possible choices. - - Returns: - :obj:`numpy.ndarray` of :obj:`float`: - A list of the choices made. - Raises: - :exc:`AssertionError`: When the number of ``thresholds`` (t) and the - number of choices (c) are not either t == c or t == c - 1. +def apply_thresholds(input, thresholds, choices): + """ + Return one of the choices depending on the input position compared to thresholds, for each input. Examples: >>> input = numpy.array([4, 5, 6, 7, 8]) @@ -38,34 +14,17 @@ def apply_thresholds( """ - condlist: List[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), \ - " ".join([ - "apply_thresholds must be called with the same number of", - "thresholds than choices, or one more choice", - ]) - + "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: ArrayLike[str], that: ArrayLike[str]) -> ArrayType[str]: - """Concatenates the values of two arrays. - - Args: - this: An array to concatenate. - that: Another array to concatenate. - - Returns: - :obj:`numpy.ndarray` of :obj:`float`: - An array with the concatenated values. +def concat(this, that): + """ Examples: >>> this = ["this", "that"] @@ -75,36 +34,18 @@ def concat(this: ArrayLike[str], that: ArrayLike[str]) -> ArrayType[str]: """ - 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.char.add(this, that) - - -def switch( - conditions: ArrayType[float], - value_by_condition: Dict[float, Any], - ) -> ArrayType[float]: - """Reproduces a switch statement. + return numpy.core.defchararray.add(this, that) - Given an array of conditions, returns an array of the same size, - replacing each condition item by the corresponding given value. - Args: - conditions: An array of conditions. - value_by_condition: Values to replace for each condition. - - Returns: - :obj:`numpy.ndarray` of :obj:`float`: - An array with the replaced values. - - Raises: - :exc:`AssertionError`: When ``value_by_condition`` is empty. +def switch(conditions, value_by_condition): + ''' + Reproduces a switch statement: given an array of conditions, return an array of the same size replacing each + condition item by the corresponding given value. Examples: >>> conditions = numpy.array([1, 1, 1, 2]) @@ -112,14 +53,12 @@ def switch( >>> switch(conditions, value_by_condition) array([80, 80, 80, 90]) - """ + ''' assert len(value_by_condition) > 0, \ "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()) diff --git a/setup.cfg b/setup.cfg index 0512b02b34..3b09cc591c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,7 @@ enable = C0115,C0116,R0401 score = no [tool:pytest] -addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.58 --doctest-modules --disable-pytest-warnings --showlocals +addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.55 --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py testpaths = openfisca_core/commons openfisca_core/types tests From fa398aaf2dc0be384901f754f9eb5735230b3d18 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 16:19:05 +0200 Subject: [PATCH 31/37] Rollback misc doc --- openfisca_core/commons/misc.py | 40 ++++++++-------------------------- setup.cfg | 3 +-- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 5056389c6e..5dd54f70c5 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,18 +1,8 @@ -from typing import TypeVar +import numpy -from openfisca_core.types import ArrayType -T = TypeVar("T") - - -def empty_clone(original: T) -> T: - """Creates an empty instance of the same class of the original object. - - Args: - original: An object to clone. - - Returns: - The cloned, empty, object. +def empty_clone(original): + """Create a new empty instance of the same class of the original object. Examples: >>> Foo = type("Foo", (list,), {}) @@ -29,33 +19,21 @@ def empty_clone(original: T) -> T: """ - Dummy: object - new: T - - Dummy = type( - "Dummy", - (original.__class__,), - {"__init__": lambda self: None}, - ) + class Dummy(original.__class__): + def __init__(self) -> None: + pass new = Dummy() new.__class__ = original.__class__ return new -def stringify_array(array: ArrayType) -> str: - """Generates a clean string representation of a numpy array. - - Args: - array: An array. - - Returns: - :obj:`str`: - "None" if the ``array`` is None, the stringified ``array`` otherwise. +def stringify_array(array: numpy.ndarray) -> str: + """ + Generate a clean string representation of a NumPY array. Examples: >>> import numpy - >>> stringify_array(None) 'None' diff --git a/setup.cfg b/setup.cfg index 3b09cc591c..2035a86a4a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,13 +14,12 @@ rst-directives = attribute, deprecated, seealso, versionadded, versionchang rst-roles = any, attr, class, exc, func, meth, obj strictness = short -; C0115: We document classes. ; C0116: We document public functions. ; R0401: We avoid cyclic imports —required for unit/doc tests. [pylint.message_control] disable = all -enable = C0115,C0116,R0401 +enable = C0116,R0401 score = no [tool:pytest] From 21de68160130768acc77cb22089cfb135f3442bc Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 16:22:13 +0200 Subject: [PATCH 32/37] Rollback rates doc --- openfisca_core/commons/rates.py | 107 +++++--------------------------- setup.cfg | 17 ++--- 2 files changed, 25 insertions(+), 99 deletions(-) diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index 487abd3aea..9148a1ec4d 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -1,36 +1,13 @@ -from typing import Optional - import numpy -from openfisca_core.types import ArrayLike, ArrayType - - -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 - income. Optionally, a ``trim`` can be applied consisting on the lower and - upper bounds of the average rate to be computed. - - Note: - Usually, ``target`` and ``varying`` are the same size. - - Args: - target: The targeted net income. - varying: The varying gross income. - trim: The lower and upper bounds of the average rate. - Returns: - :obj:`numpy.ndarray` of :obj:`float`: +def average_rate(target = None, varying = None, trim = None): + ''' + Computes the average rate of a targeted net income, according to the varying gross income. - The average rate for each target. - - When ``trim`` is provided, values that are out of the provided bounds - are replaced by :obj:`numpy.nan`. + :param target: Targeted net income, numerator + :param varying: Varying gross income, denominator + :param trim: Lower and upper bound of average rate to return Examples: >>> target = numpy.array([1, 2, 3]) @@ -39,55 +16,18 @@ def average_rate( >>> average_rate(target, varying, trim) array([ nan, 0. , -0.5]) - """ - - 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: 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 - income. Optionally, a ``trim`` can be applied consisting of the lower and - upper bounds of the marginal rate to be computed. - - Note: - Usually, ``target`` and ``varying`` are the same size. - - Args: - target: The targeted net income. - varying: The varying gross income. - trim: The lower and upper bounds of the marginal rate. - - Returns: - :obj:`numpy.ndarray` of :obj:`float`: - - The marginal rate for each target. - - When ``trim`` is provided, values that are out of the provided bounds - are replaced by :obj:`numpy.nan`. +def marginal_rate(target = None, varying = None, trim = None): + """ Examples: >>> target = numpy.array([1, 2, 3]) @@ -98,27 +38,10 @@ def marginal_rate( """ - marginal_rate: ArrayType[float] - - marginal_rate = ( - + 1 - - (target[:-1] - - target[1:]) / (varying[:-1] - - varying[1:]) - ) - + # target: numerator, varying: denominator + 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 diff --git a/setup.cfg b/setup.cfg index 2035a86a4a..20cdf46bfc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,15 @@ -; E128/133: We prefer hang-closing visual indents -; E251: We prefer `function(x = 1)` over `function(x=1)` -; E501: We do not enforce a maximum line length -; F403/405: We ignore * imports -; W503/504: We break lines before binary operators (Knuth's style) +; DXXX: http://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping. +; DAR101: We do not (yet) document class/function attributes/arguments. +; DAR201: We do not (yet) document method/function returns. +; E128/133: We prefer hang-closing visual indents. +; E251: We prefer `function(x = 1)` over `function(x=1)`. +; E501: We do not enforce a maximum line length. +; F403/405: We ignore * imports. +; W503/504: We break lines before binary operators (Knuth's style). [flake8] hang-closing = true -extend-ignore = D +extend-ignore = D,DAR101,DAR201 ignore = E128,E251,F403,F405,E501,W503,W504 in-place = true include-in-doctest = openfisca_core/commons openfisca_core/types @@ -23,7 +26,7 @@ enable = C0116,R0401 score = no [tool:pytest] -addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.55 --doctest-modules --disable-pytest-warnings --showlocals +addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.54 --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py testpaths = openfisca_core/commons openfisca_core/types tests From f3b5ed3f715d3893817ec2b995566dfa483bf10f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 16:34:12 +0200 Subject: [PATCH 33/37] Rollback mypy update --- setup.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 432205cfd9..35c296b4b0 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ general_requirements = [ 'dpath >= 1.5.0, < 2.0.0', - 'nptyping >= 1.4.3, < 2.0.0', 'numexpr >= 2.7.0, <= 3.0', 'numpy >= 1.11, < 1.21', 'psutil >= 5.4.7, < 6.0.0', @@ -32,13 +31,11 @@ 'flake8-docstrings == 1.6.0', 'flake8-print >= 3.1.0, < 4.0.0', 'flake8-rst-docstrings < 1.0.0', - 'mypy == 0.910', - 'pylint == 2.10.2', + 'mypy >= 0.701, < 0.800', 'openfisca-country-template >= 3.10.0, < 4.0.0', 'openfisca-extension-template >= 1.2.0rc0, < 2.0.0', + 'pylint == 2.10.2', 'pytest-cov >= 2.6.1, < 3.0.0', - 'types-PyYAML == 5.4.10', - 'types-setuptools == 57.0.2', ] + api_requirements setup( From f0d1b96c8d68e4f85257632474374b791fa28375 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 16:37:50 +0200 Subject: [PATCH 34/37] Rollback add types --- openfisca_core/types/__init__.py | 46 ----------------- openfisca_core/types/data_types/__init__.py | 1 - openfisca_core/types/data_types/arrays.py | 56 --------------------- setup.cfg | 4 +- 4 files changed, 2 insertions(+), 105 deletions(-) delete mode 100644 openfisca_core/types/__init__.py delete mode 100644 openfisca_core/types/data_types/__init__.py delete mode 100644 openfisca_core/types/data_types/arrays.py diff --git a/openfisca_core/types/__init__.py b/openfisca_core/types/__init__.py deleted file mode 100644 index 36dabd1899..0000000000 --- a/openfisca_core/types/__init__.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Data types and protocols used by OpenFisca Core. - -The type definitions included in this sub-package are intented mostly for -contributors, to help them better document contracts and 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, - ) - -#: Official Public API - -__all__ = ["ArrayLike", "ArrayType"] diff --git a/openfisca_core/types/data_types/__init__.py b/openfisca_core/types/data_types/__init__.py deleted file mode 100644 index 6dd38194e3..0000000000 --- a/openfisca_core/types/data_types/__init__.py +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 3e941b4ab1..0000000000 --- a/openfisca_core/types/data_types/arrays.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import Sequence, TypeVar, Union - -from nptyping import NDArray as ArrayType - -T = TypeVar("T", bool, bytes, float, int, object, str) - -A = Union[ - ArrayType[bool], - ArrayType[bytes], - ArrayType[float], - ArrayType[int], - ArrayType[object], - ArrayType[str], - ] - -ArrayLike = Union[A, 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 - -""" diff --git a/setup.cfg b/setup.cfg index 20cdf46bfc..c8bcec9fa9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,10 +26,10 @@ enable = C0116,R0401 score = no [tool:pytest] -addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.54 --doctest-modules --disable-pytest-warnings --showlocals +addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.50 --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py -testpaths = openfisca_core/commons openfisca_core/types tests +testpaths = openfisca_core/commons tests [mypy] ignore_missing_imports = True From 1577293ac0cfdc474673f637e750b7502a0e7db3 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 29 Sep 2021 16:42:33 +0200 Subject: [PATCH 35/37] Remove typing extensions --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 35c296b4b0..dec6f2bcf6 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ 'pytest >= 4.4.1, < 6.0.0', # For openfisca test 'PyYAML >= 3.10', 'sortedcontainers == 2.2.2', - 'typing-extensions >= 3.0.0.0, < 4.0.0.0', ] api_requirements = [ From f70e66f096977ec3e345c3184976dcd4410fe8bd Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 7 Oct 2021 15:31:58 +0200 Subject: [PATCH 36/37] Apply suggestions from code review Co-authored-by: Matti Schneider --- openfisca_tasks/lint.mk | 12 ++++++------ setup.cfg | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 2e52457926..a2fe4b03a0 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-styling-doc check-types +lint: check-syntax-errors check-style lint-doc check-types @$(call print_pass,$@:) ## Compile python files to check for syntax errors. @@ -15,18 +15,18 @@ check-style: $(shell git ls-files "*.py") @$(call print_pass,$@:) ## Run linters to check for syntax and style errors in the doc. -lint-styling-doc: \ - lint-styling-doc-commons \ - lint-styling-doc-types \ +lint-doc: \ + lint-doc-commons \ + lint-doc-types \ ; ## Run linters to check for syntax and style errors in the doc. -lint-styling-doc-%: +lint-doc-%: @## These checks are exclusively related to doc/strings/test. @## @## They can be integrated into setup.cfg once all checks pass. @## The reason they're here is because otherwise we wouldn't be - @## able to integrate documentation improvements progresively. + @## able to integrate documentation improvements incrementally. @## @## D101: Each class has to have at least one doctest. @## D102: Each public method has to have at least one doctest. diff --git a/setup.cfg b/setup.cfg index c8bcec9fa9..75004da81c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ -; DXXX: http://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping. +; DXXX: We do not (yet) check docstrings (see https://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping). ; DAR101: We do not (yet) document class/function attributes/arguments. ; DAR201: We do not (yet) document method/function returns. ; E128/133: We prefer hang-closing visual indents. @@ -26,7 +26,7 @@ enable = C0116,R0401 score = no [tool:pytest] -addopts = --cov-report=term-missing:skip-covered --cov-fail-under=78.50 --doctest-modules --disable-pytest-warnings --showlocals +addopts = --cov-report=term-missing:skip-covered --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py testpaths = openfisca_core/commons tests From 8e8efe4897aa4200aec2b9f052cb1d2669464b21 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 20 Sep 2021 00:37:49 +0200 Subject: [PATCH 37/37] Bump patch to 35.5.4 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fed09d73c6..f6a7f9b5dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +### 35.5.4 [#1033](https://github.com/openfisca/openfisca-core/pull/1033) + +#### Bug Fixes + +- Fix doctests of the commons module + +#### Dependencies + +- `darglint`, `flake8-docstrings`, & `pylint` + - For automatic docstring linting & validation. + ### 35.5.3 [#1020](https://github.com/openfisca/openfisca-core/pull/1020) #### Technical changes diff --git a/setup.py b/setup.py index dec6f2bcf6..66b49c1377 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ setup( name = 'OpenFisca-Core', - version = '35.5.3', + version = '35.5.4', author = 'OpenFisca Team', author_email = 'contact@openfisca.org', classifiers = [