From 56af1b379ba9cf768c785d4f332cfe58bbf74579 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 27 Jun 2023 04:00:13 +0200 Subject: [PATCH 001/188] Add role description --- openfisca_core/entities/role.py | 64 ++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 048671c59f..00dd40c1eb 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,7 +1,19 @@ +from __future__ import annotations + +import dataclasses import textwrap class Role: + """The role of an Entity within a GroupEntity. + + Each Entity related to a GroupEntity has a Role. For example, if you have + a family, its roles could include a parent, a child, and so on. Or if you + have a tax household, its roles could include the taxpayer, a spouse, + several dependents, and the like. + + """ + def __init__(self, description, entity): self.entity = entity self.key = description["key"] @@ -11,5 +23,55 @@ def __init__(self, description, entity): self.max = description.get("max") self.subroles = None - def __repr__(self): + def __repr__(self) -> str: return "Role({})".format(self.key) + + +@dataclasses.dataclass(frozen=True) +class RoleDescription: + """A Role's description. + + Examples: + >>> description = { + ... "key": "parent", + ... "label": "Parents", + ... "plural": "parents", + ... "doc": "\t\t\tThe one/two adults in charge of the household.", + ... "max": 2, + ... } + + >>> role_description = RoleDescription(**description) + + >>> repr(RoleDescription) + "" + + >>> repr(role_description) + "RoleDescription(key='parent', plural='parents', label='Parents', ...)" + + >>> str(role_description) + "RoleDescription(key='parent', plural='parents', label='Parents', ...)" + + >>> role_description.key + 'parent' + + .. versionadded:: 40.1.0 + + """ + + #: A key to identify the Role. + key: str + + #: The ``key``, pluralised. + plural: str | None + + #: A summary description. + label: str | None + + #: A full description, dedented. + doc: str = "" + + #: Max number of members. + max: int | None = None + + def __post_init__(self) -> None: + object.__setattr__(self, "doc", textwrap.dedent(self.doc)) From 4e0792a52d79ac497dad2d50d638ffccd062517d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 27 Jun 2023 04:55:23 +0200 Subject: [PATCH 002/188] Delegate role's description --- openfisca_core/entities/role.py | 56 +++++++++++++++++++++++++++------ setup.cfg | 2 +- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 00dd40c1eb..b12c260e10 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,8 +1,12 @@ from __future__ import annotations +from typing import Any + import dataclasses import textwrap +from openfisca_core.types import Entity + class Role: """The role of an Entity within a GroupEntity. @@ -12,20 +16,49 @@ class Role: have a tax household, its roles could include the taxpayer, a spouse, several dependents, and the like. + Attributes: + entity (Entity): The Entity to which the Role belongs. + subroles (list[Role]): A list of subroles. + description (RoleDescription): A description of the Role. + + Args: + description (dict): A description of the Role. + entity (Entity): The Entity to which the Role belongs. + + Examples: + >>> role = Role({"key": "parent"}, object()) + + >>> repr(Role) + "" + + >>> repr(role) + 'Role(parent)' + + >>> str(role) + 'Role(parent)' + + >>> role.key + 'parent' + + .. versionchanged:: 40.1.0 + Added documentation, doctests, and typing. + """ - def __init__(self, description, entity): - self.entity = entity - self.key = description["key"] - self.label = description.get("label") - self.plural = description.get("plural") - self.doc = textwrap.dedent(description.get("doc", "")) - self.max = description.get("max") - self.subroles = None + def __init__(self, description: dict[str, Any], entity: Entity) -> None: + self.entity: Entity = entity + self.subroles: list[Role] | None = None + self.description: RoleDescription = RoleDescription(**description) def __repr__(self) -> str: return "Role({})".format(self.key) + def __getattr__(self, attr: str) -> Any: + if hasattr(self.description, attr): + return getattr(self.description, attr) + + raise AttributeError + @dataclasses.dataclass(frozen=True) class RoleDescription: @@ -62,10 +95,10 @@ class RoleDescription: key: str #: The ``key``, pluralised. - plural: str | None + plural: str | None = None #: A summary description. - label: str | None + label: str | None = None #: A full description, dedented. doc: str = "" @@ -73,5 +106,8 @@ class RoleDescription: #: Max number of members. max: int | None = None + #: A list of subroles. + subroles: list[str] | None = None + def __post_init__(self) -> None: object.__setattr__(self, "doc", textwrap.dedent(self.doc)) diff --git a/setup.cfg b/setup.cfg index df758aa180..ac338f8558 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ extend-ignore = # hang-closing = true # ignore = E128,E251,F403,F405,E501,RST301,W503,W504 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/holders openfisca_core/periods openfisca_core/types +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/types # Recommend matching the black line length (default 88), # rather than using the flake8 default of 79: max-line-length = 88 From 7aaff06377c6ebc93b5b1264439cb794b75fe764 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 27 Jun 2023 05:20:17 +0200 Subject: [PATCH 003/188] Leave description as serializer --- openfisca_core/entities/role.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index b12c260e10..678f3a1fc2 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -18,8 +18,12 @@ class Role: Attributes: entity (Entity): The Entity to which the Role belongs. + key (str): A key to identify the Role. + plural (str): The ``key``, pluralised. + label (str): A summary description. + doc (str): A full description, dedented. + max (int): Max number of members. subroles (list[Role]): A list of subroles. - description (RoleDescription): A description of the Role. Args: description (dict): A description of the Role. @@ -46,19 +50,18 @@ class Role: """ def __init__(self, description: dict[str, Any], entity: Entity) -> None: + description: RoleDescription = RoleDescription(**description) self.entity: Entity = entity + self.key: str = description.key + self.plural: str | None = description.plural + self.label: str | None = description.label + self.doc: str = description.doc + self.max: int | None = description.max self.subroles: list[Role] | None = None - self.description: RoleDescription = RoleDescription(**description) def __repr__(self) -> str: return "Role({})".format(self.key) - def __getattr__(self, attr: str) -> Any: - if hasattr(self.description, attr): - return getattr(self.description, attr) - - raise AttributeError - @dataclasses.dataclass(frozen=True) class RoleDescription: From 7cc8ebab24ab7e37e64b98824a284971fbe8ff38 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 27 Jun 2023 05:29:22 +0200 Subject: [PATCH 004/188] Make role an entity (DDD) --- openfisca_core/entities/role.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 678f3a1fc2..3ab026aa0c 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -59,6 +59,12 @@ def __init__(self, description: dict[str, Any], entity: Entity) -> None: self.max: int | None = description.max self.subroles: list[Role] | None = None + def __eq__(self, other: object) -> bool: + if not isinstance(other, Role): + return NotImplemented + + return self.entity == other.entity and self.key == other.key + def __repr__(self) -> str: return "Role({})".format(self.key) From f829f880e579356b75fa47e2123665dbf5715de2 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 27 Jun 2023 05:36:02 +0200 Subject: [PATCH 005/188] Keep role description private --- openfisca_core/entities/role.py | 18 +++++++++--------- openfisca_tasks/test_code.mk | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 3ab026aa0c..f87cfbb388 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -44,13 +44,13 @@ class Role: >>> role.key 'parent' - .. versionchanged:: 40.1.0 + .. versionchanged:: 40.1.1 Added documentation, doctests, and typing. """ def __init__(self, description: dict[str, Any], entity: Entity) -> None: - description: RoleDescription = RoleDescription(**description) + description: _RoleDescription = _RoleDescription(**description) self.entity: Entity = entity self.key: str = description.key self.plural: str | None = description.plural @@ -70,7 +70,7 @@ def __repr__(self) -> str: @dataclasses.dataclass(frozen=True) -class RoleDescription: +class _RoleDescription: """A Role's description. Examples: @@ -82,21 +82,21 @@ class RoleDescription: ... "max": 2, ... } - >>> role_description = RoleDescription(**description) + >>> role_description = _RoleDescription(**description) - >>> repr(RoleDescription) - "" + >>> repr(_RoleDescription) + "" >>> repr(role_description) - "RoleDescription(key='parent', plural='parents', label='Parents', ...)" + "_RoleDescription(key='parent', plural='parents', label='Parents', ...)" >>> str(role_description) - "RoleDescription(key='parent', plural='parents', label='Parents', ...)" + "_RoleDescription(key='parent', plural='parents', label='Parents', ...)" >>> role_description.key 'parent' - .. versionadded:: 40.1.0 + .. versionadded:: 40.1.1 """ diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index c60c294bf7..33bccf7a76 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -33,6 +33,7 @@ test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 @$(call print_help,$@:) @pytest --quiet --capture=no --xdoctest --xdoctest-verbose=0 \ openfisca_core/commons \ + openfisca_core/entities \ openfisca_core/holders \ openfisca_core/periods \ openfisca_core/types From 56519edb9558e5987baaf90990e6b423227410d0 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Sep 2023 14:30:04 +0200 Subject: [PATCH 006/188] Fix linter --- openfisca_core/entities/entity.py | 2 +- .../taxbenefitsystems/tax_benefit_system.py | 8 ++++---- openfisca_core/variables/variable.py | 2 +- openfisca_web_api/loader/variables.py | 16 ++++------------ .../tax_scales/test_abstract_rate_tax_scale.py | 2 +- tests/core/tax_scales/test_abstract_tax_scale.py | 2 +- 6 files changed, 12 insertions(+), 20 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index b5344dbe58..62c803bd82 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -24,7 +24,7 @@ def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystem): self._tax_benefit_system = tax_benefit_system def check_role_validity(self, role: Any) -> None: - if role is not None and not type(role) == Role: + if role is not None and not isinstance(role, Role): raise ValueError("{} is not a valid role".format(role)) def get_variable( diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 271f77ba5e..812448ae1d 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -236,7 +236,7 @@ def add_variables_from_file(self, file_path): sys.modules[module_name] = module lines = linecache.getlines(file_path, module.__dict__) - source = ''.join(lines) + source = "".join(lines) tree = ast.parse(source) defs = {i.name: i for i in tree.body if isinstance(i, ast.ClassDef)} spec.loader.exec_module(module) @@ -262,9 +262,9 @@ def add_variables_from_file(self, file_path): class_def = defs[pot_variable.__name__] pot_variable.introspection_data = ( source_file_path, - ''.join(lines[class_def.lineno-1:class_def.end_lineno]), - class_def.lineno-1 - ) + "".join(lines[class_def.lineno - 1 : class_def.end_lineno]), + class_def.lineno - 1, + ) self.add_variable(pot_variable) except Exception: log.error( diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index f0a88ea8df..33cae3b20d 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -368,7 +368,7 @@ def get_introspection_data(cls): try: return cls.introspection_data except AttributeError: - return '', None, 0 + return "", None, 0 def get_formula( self, diff --git a/openfisca_web_api/loader/variables.py b/openfisca_web_api/loader/variables.py index ae6054c058..f9b6e05887 100644 --- a/openfisca_web_api/loader/variables.py +++ b/openfisca_web_api/loader/variables.py @@ -38,9 +38,7 @@ def build_source_url( ) -def build_formula( - formula, country_package_metadata, source_file_path -): +def build_formula(formula, country_package_metadata, source_file_path): source_code, start_line_number = inspect.getsourcelines(formula) source_code = textwrap.dedent("".join(source_code)) @@ -58,13 +56,9 @@ def build_formula( return api_formula -def build_formulas( - formulas, country_package_metadata, source_file_path -): +def build_formulas(formulas, country_package_metadata, source_file_path): return { - start_date: build_formula( - formula, country_package_metadata, source_file_path - ) + start_date: build_formula(formula, country_package_metadata, source_file_path) for start_date, formula in formulas.items() } @@ -97,9 +91,7 @@ def build_variable(variable, country_package_metadata): if len(variable.formulas) > 0: result["formulas"] = build_formulas( - variable.formulas, - country_package_metadata, - source_file_path + variable.formulas, country_package_metadata, source_file_path ) if variable.end: diff --git a/tests/core/tax_scales/test_abstract_rate_tax_scale.py b/tests/core/tax_scales/test_abstract_rate_tax_scale.py index 3d284a49e9..3d906dfcb4 100644 --- a/tests/core/tax_scales/test_abstract_rate_tax_scale.py +++ b/tests/core/tax_scales/test_abstract_rate_tax_scale.py @@ -6,4 +6,4 @@ def test_abstract_tax_scale(): with pytest.warns(DeprecationWarning): result = taxscales.AbstractRateTaxScale() - assert type(result) == taxscales.AbstractRateTaxScale + assert isinstance(result, taxscales.AbstractRateTaxScale) diff --git a/tests/core/tax_scales/test_abstract_tax_scale.py b/tests/core/tax_scales/test_abstract_tax_scale.py index f6834e7dc7..7746ea03ae 100644 --- a/tests/core/tax_scales/test_abstract_tax_scale.py +++ b/tests/core/tax_scales/test_abstract_tax_scale.py @@ -6,4 +6,4 @@ def test_abstract_tax_scale(): with pytest.warns(DeprecationWarning): result = taxscales.AbstractTaxScale() - assert type(result) == taxscales.AbstractTaxScale + assert isinstance(result, taxscales.AbstractTaxScale) From 70971aeaa26070bfea352cfd3b71c0a032a50cdf Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Sep 2023 14:33:30 +0200 Subject: [PATCH 007/188] Fix version changed in Role --- openfisca_core/entities/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index f87cfbb388..235863e9c9 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -44,7 +44,7 @@ class Role: >>> role.key 'parent' - .. versionchanged:: 40.1.1 + .. versionchanged:: 41.0.1 Added documentation, doctests, and typing. """ From c93f68a6588fd866f7eb72e85848286dc5b00292 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Sep 2023 22:16:02 +0200 Subject: [PATCH 008/188] Fix shadowing of variable --- openfisca_core/entities/role.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 235863e9c9..590f023e14 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -50,13 +50,13 @@ class Role: """ def __init__(self, description: dict[str, Any], entity: Entity) -> None: - description: _RoleDescription = _RoleDescription(**description) + role_description: _RoleDescription = _RoleDescription(**description) self.entity: Entity = entity - self.key: str = description.key - self.plural: str | None = description.plural - self.label: str | None = description.label - self.doc: str = description.doc - self.max: int | None = description.max + self.key: str = role_description.key + self.plural: str | None = role_description.plural + self.label: str | None = role_description.label + self.doc: str = role_description.doc + self.max: int | None = role_description.max self.subroles: list[Role] | None = None def __eq__(self, other: object) -> bool: From 80edf50b13beddb5e8ab6cadcd2fc487c31c5ac9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Sep 2023 22:58:18 +0200 Subject: [PATCH 009/188] Small fixes --- openfisca_core/entities/role.py | 12 ++++++------ openfisca_core/entities/typing.py | 9 +++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 openfisca_core/entities/typing.py diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 590f023e14..3ca27f82dd 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -5,7 +5,7 @@ import dataclasses import textwrap -from openfisca_core.types import Entity +from .typing import Entity class Role: @@ -66,7 +66,7 @@ def __eq__(self, other: object) -> bool: return self.entity == other.entity and self.key == other.key def __repr__(self) -> str: - return "Role({})".format(self.key) + return f"Role({self.key})" @dataclasses.dataclass(frozen=True) @@ -88,15 +88,15 @@ class _RoleDescription: "" >>> repr(role_description) - "_RoleDescription(key='parent', plural='parents', label='Parents', ...)" + "_RoleDescription(key='parent', plural='parents', label='Parents',...)" >>> str(role_description) - "_RoleDescription(key='parent', plural='parents', label='Parents', ...)" + "_RoleDescription(key='parent', plural='parents', label='Parents',...)" >>> role_description.key 'parent' - .. versionadded:: 40.1.1 + .. versionadded:: 41.0.1 """ @@ -109,7 +109,7 @@ class _RoleDescription: #: A summary description. label: str | None = None - #: A full description, dedented. + #: A full description, non-indented. doc: str = "" #: Max number of members. diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py new file mode 100644 index 0000000000..ab2640d21a --- /dev/null +++ b/openfisca_core/entities/typing.py @@ -0,0 +1,9 @@ +from typing import Protocol + + +class Entity(Protocol): + """Entity protocol. + + .. versionadded:: 41.0.1 + + """ From 8dec6b4aea8c9b3bdd08b8e5949326ba68cb7eee Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 27 Jun 2023 05:38:11 +0200 Subject: [PATCH 010/188] Bump version --- CHANGELOG.md | 10 ++++++++-- setup.py | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b913a9ba11..499914c614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.0.1 [#1187](https://github.com/openfisca/openfisca-core/pull/1187) + +#### Technical changes + +- Document `Role`. + # 41.0.0 [#1189](https://github.com/openfisca/openfisca-core/pull/1189) #### Breaking changes @@ -12,8 +18,8 @@ The Web API was very prone to crashing, timeouting at startup because of the tim #### New Features -* Allows for dispatching and dividing inputs over a broader range. - * For example, divide a monthly variable by week. +- Allows for dispatching and dividing inputs over a broader range. + - For example, divide a monthly variable by week. ### 40.0.1 [#1184](https://github.com/openfisca/openfisca-core/pull/1184) diff --git a/setup.py b/setup.py index f00da6b793..1a399b33d2 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.0.0", + version="41.0.1", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From b3503d886b18f1bd302d38a711b6fbee688459c9 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Mon, 2 Oct 2023 15:52:08 +0100 Subject: [PATCH 011/188] Add __hash__ for Role --- openfisca_core/entities/role.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 3ca27f82dd..abd68a6f29 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -65,6 +65,9 @@ def __eq__(self, other: object) -> bool: return self.entity == other.entity and self.key == other.key + def __hash__(self): + return hash(f"{self.entity.key}_{self.key}") + def __repr__(self) -> str: return f"Role({self.key})" From f9de7625c14091dea807477e7a9a8e65b6b460a0 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Tue, 3 Oct 2023 10:05:10 +0100 Subject: [PATCH 012/188] Bump --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 499914c614..4a54d3b3f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.0.2 [#1194](https://github.com/openfisca/openfisca-core/pull/1194) + +#### Technical changes + +- Add `__hash__` method to `Role`. + ### 41.0.1 [#1187](https://github.com/openfisca/openfisca-core/pull/1187) #### Technical changes diff --git a/setup.py b/setup.py index 1a399b33d2..35621f6e1b 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.0.1", + version="41.0.2", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 31d375a316b14dcac10dabf9fbe9411d650011a0 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 3 Oct 2023 15:34:32 +0200 Subject: [PATCH 013/188] Make role hashable --- openfisca_core/entities/role.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index abd68a6f29..5577da3655 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -41,12 +41,12 @@ class Role: >>> str(role) 'Role(parent)' + >>> {role} + {Role(parent)} + >>> role.key 'parent' - .. versionchanged:: 41.0.1 - Added documentation, doctests, and typing. - """ def __init__(self, description: dict[str, Any], entity: Entity) -> None: @@ -59,15 +59,6 @@ def __init__(self, description: dict[str, Any], entity: Entity) -> None: self.max: int | None = role_description.max self.subroles: list[Role] | None = None - def __eq__(self, other: object) -> bool: - if not isinstance(other, Role): - return NotImplemented - - return self.entity == other.entity and self.key == other.key - - def __hash__(self): - return hash(f"{self.entity.key}_{self.key}") - def __repr__(self) -> str: return f"Role({self.key})" @@ -96,6 +87,9 @@ class _RoleDescription: >>> str(role_description) "_RoleDescription(key='parent', plural='parents', label='Parents',...)" + >>> {role_description} + {...} + >>> role_description.key 'parent' From df283ed038c6411a72680c48f9d01d4449664a1d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 3 Oct 2023 15:46:48 +0200 Subject: [PATCH 014/188] Bump version --- CHANGELOG.md | 13 +++++++++++++ setup.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a54d3b3f2..8d3d72ebc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 41.1.0 [#1195](https://github.com/openfisca/openfisca-core/pull/1195) + +#### Technical changes + +- Make `Role` explicitly hashable. +- Details: + - By introducing `__eq__`, naturally `Role` became unhashable, because + equality was calculated based on a property of `Role` + (`role.key == another_role.key`), and no longer structurally + (`"1" == "1"`). + - This changeset removes `__eq__`, as `Role` is being used downstream as a + hashable object, and adds a test to ensure `Role`'s hashability. + ### 41.0.2 [#1194](https://github.com/openfisca/openfisca-core/pull/1194) #### Technical changes diff --git a/setup.py b/setup.py index 35621f6e1b..15e58543ce 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.0.2", + version="41.1.0", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 359a27ab382c70803e8045daa02abdc91149b6db Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 26 Jun 2023 13:41:04 +0200 Subject: [PATCH 015/188] Skip type-checking tasks --- openfisca_tasks/lint.mk | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index a15b704576..0494b2056e 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-typing-strict +lint: check-syntax-errors check-style lint-doc @$(call print_pass,$@:) ## Compile python files to check for syntax errors. @@ -35,10 +35,9 @@ lint-doc-%: ## Run static type checkers for type errors. check-types: - @echo " check-types: Temporarily dismissed" -# @$(call print_help,$@:) -# @mypy --package openfisca_core --package openfisca_web_api -# @$(call print_pass,$@:) + @$(call print_help,$@:) + @mypy --package openfisca_core --package openfisca_web_api + @$(call print_pass,$@:) ## Run static type checkers for type errors (strict). lint-typing-strict: \ @@ -48,14 +47,13 @@ lint-typing-strict: \ ## Run static type checkers for type errors (strict). lint-typing-strict-%: - @echo " lint-typing-strict: Temporarily dismissed" -# @$(call print_help,$(subst $*,%,$@:)) -# @mypy \ -# --cache-dir .mypy_cache-openfisca_core.$* \ -# --implicit-reexport \ -# --strict \ -# --package openfisca_core.$* -# @$(call print_pass,$@:) + @$(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") From 73e2332f411625065bfe924be8dbb9b639f6223b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 26 Jun 2023 13:46:26 +0200 Subject: [PATCH 016/188] Remove type-checking state on CI --- .github/workflows/workflow.yml | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 7fc232b777..b32fb84202 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -113,30 +113,6 @@ jobs: - name: Run Extension Template tests run: make test-extension - check-numpy: - runs-on: ubuntu-22.04 - needs: [ build ] - env: - TERM: xterm-256color - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Check NumPy typing against latest 3 minor versions - run: for i in {1..3}; do VERSION=$(${GITHUB_WORKSPACE}/.github/get-numpy-version.py prev) && pip install numpy==$VERSION && make check-types; done - lint-files: runs-on: ubuntu-22.04 needs: [ build ] From 6ef627f3449e555e3af3719dec53ffe69b51b0d5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Sep 2023 14:56:05 +0200 Subject: [PATCH 017/188] Remove check numpy --- .github/get-numpy-version.py | 38 ---------------------------------- .github/workflows/workflow.yml | 2 +- 2 files changed, 1 insertion(+), 39 deletions(-) delete mode 100755 .github/get-numpy-version.py diff --git a/.github/get-numpy-version.py b/.github/get-numpy-version.py deleted file mode 100755 index 8ae0bc1386..0000000000 --- a/.github/get-numpy-version.py +++ /dev/null @@ -1,38 +0,0 @@ -#! /usr/bin/env python - -from __future__ import annotations - -import os -import sys -import typing -from packaging import version -from typing import NoReturn, Union - -import numpy - -if typing.TYPE_CHECKING: - from packaging.version import LegacyVersion, Version - - -def prev() -> NoReturn: - release = _installed().release - - if release is None: - sys.exit(os.EX_DATAERR) - - major, minor, _ = release - - if minor == 0: - sys.exit(os.EX_DATAERR) - - minor -= 1 - print(f"{major}.{minor}.0") # noqa: T201 - sys.exit(os.EX_OK) - - -def _installed() -> Union[LegacyVersion, Version]: - return version.parse(numpy.__version__) - - -if __name__ == "__main__": - globals()[sys.argv[1]]() diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index b32fb84202..b38c2ce258 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -141,7 +141,7 @@ jobs: check-version: runs-on: ubuntu-22.04 - needs: [ test-core, test-country-template, test-extension-template, check-numpy, lint-files ] # Last job to run + needs: [ test-core, test-country-template, test-extension-template, lint-files ] # Last job to run steps: - uses: actions/checkout@v2 From 1e8d38c2bb954b04a77069efbb761b29dbd6c2bc Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Sep 2023 15:01:38 +0200 Subject: [PATCH 018/188] Fix linter --- openfisca_core/commons/formulas.py | 3 +- openfisca_core/commons/misc.py | 3 +- openfisca_core/commons/rates.py | 3 +- .../data_storage/on_disk_storage.py | 2 +- openfisca_core/entities/__init__.py | 4 +- openfisca_core/entities/entity.py | 4 +- openfisca_core/entities/group_entity.py | 3 +- openfisca_core/errors/__init__.py | 46 +++++++---- openfisca_core/errors/empty_argument_error.py | 3 +- openfisca_core/holders/holder.py | 14 ++-- openfisca_core/indexed_enums/__init__.py | 2 +- openfisca_core/indexed_enums/enum.py | 6 +- openfisca_core/model_api.py | 81 ++++++++++++------- openfisca_core/parameters/__init__.py | 54 ++++++++----- openfisca_core/parameters/config.py | 12 ++- openfisca_core/parameters/parameter.py | 5 +- .../parameters/parameter_at_instant.py | 3 +- openfisca_core/parameters/parameter_node.py | 9 ++- openfisca_core/parameters/parameter_scale.py | 5 +- openfisca_core/parameters/values_history.py | 8 +- openfisca_core/periods/period_.py | 2 +- openfisca_core/populations/__init__.py | 7 +- .../populations/group_population.py | 3 +- openfisca_core/populations/population.py | 2 +- openfisca_core/projectors/__init__.py | 4 +- .../projectors/entity_to_person_projector.py | 2 +- .../first_person_to_entity_projector.py | 2 +- .../unique_role_to_entity_projector.py | 2 +- openfisca_core/scripts/__init__.py | 2 +- openfisca_core/scripts/find_placeholders.py | 2 +- .../measure_numpy_condition_notations.py | 3 +- .../scripts/measure_performances.py | 5 +- .../measure_performances_fancy_indexing.py | 2 - .../xml_to_yaml_country_template.py | 5 +- .../xml_to_yaml_extension_template.py | 5 +- .../scripts/migrations/v24_to_25.py | 5 +- openfisca_core/scripts/openfisca_command.py | 2 +- openfisca_core/scripts/remove_fuzzy.py | 3 +- openfisca_core/scripts/run_test.py | 4 +- .../scripts/simulation_generator.py | 3 +- openfisca_core/simulations/simulation.py | 2 +- .../simulations/simulation_builder.py | 6 +- .../taxbenefitsystems/tax_benefit_system.py | 11 +-- openfisca_core/taxscales/__init__.py | 12 +-- .../taxscales/abstract_rate_tax_scale.py | 3 +- .../taxscales/abstract_tax_scale.py | 3 +- .../taxscales/amount_tax_scale_like.py | 6 +- openfisca_core/taxscales/helpers.py | 3 +- .../linear_average_rate_tax_scale.py | 6 +- .../taxscales/marginal_amount_tax_scale.py | 2 +- .../taxscales/marginal_rate_tax_scale.py | 6 +- .../taxscales/rate_tax_scale_like.py | 6 +- openfisca_core/taxscales/tax_scale_like.py | 3 +- openfisca_core/tools/simulation_dumper.py | 2 +- openfisca_core/tools/test_runner.py | 2 +- openfisca_core/tracers/computation_log.py | 3 +- openfisca_core/tracers/full_tracer.py | 3 +- openfisca_core/tracers/performance_log.py | 3 +- openfisca_core/tracers/trace_node.py | 3 +- openfisca_core/types/__init__.py | 6 +- openfisca_core/types/_data.py | 3 +- openfisca_core/types/_domain.py | 1 - openfisca_core/variables/__init__.py | 2 +- openfisca_core/variables/config.py | 1 - openfisca_core/variables/helpers.py | 3 +- openfisca_core/variables/variable.py | 2 +- openfisca_tasks/lint.mk | 2 +- openfisca_web_api/app.py | 10 +-- openfisca_web_api/handlers.py | 3 +- openfisca_web_api/loader/__init__.py | 4 +- openfisca_web_api/loader/spec.py | 3 +- .../loader/tax_benefit_system.py | 2 +- openfisca_web_api/scripts/serve.py | 4 +- setup.cfg | 36 +++------ setup.py | 3 +- .../test_parameter_validation.py | 4 +- .../test_fancy_indexing.py | 4 +- .../test_abstract_rate_tax_scale.py | 4 +- .../tax_scales/test_abstract_tax_scale.py | 4 +- .../test_linear_average_rate_tax_scale.py | 6 +- .../test_marginal_amount_tax_scale.py | 8 +- .../test_marginal_rate_tax_scale.py | 6 +- .../test_single_amount_tax_scale.py | 8 +- .../tax_scales/test_tax_scales_commons.py | 6 +- tests/core/test_axes.py | 1 - tests/core/test_formulas.py | 5 +- tests/core/test_holders.py | 5 +- tests/core/test_opt_out_cache.py | 1 - tests/core/test_parameters.py | 2 +- tests/core/test_reforms.py | 5 +- tests/core/test_simulation_builder.py | 3 +- tests/core/test_tracers.py | 15 ++-- tests/core/test_yaml.py | 2 +- .../tools/test_runner/test_yaml_runner.py | 15 ++-- tests/core/variables/test_variables.py | 3 +- tests/web_api/basic_case/__init__.py | 3 +- .../case_with_extension/test_extensions.py | 2 +- .../web_api/case_with_reform/test_reforms.py | 1 + tests/web_api/loader/test_parameters.py | 3 +- tests/web_api/test_calculate.py | 5 +- tests/web_api/test_entities.py | 3 +- tests/web_api/test_helpers.py | 4 +- tests/web_api/test_parameters.py | 4 +- tests/web_api/test_spec.py | 4 +- tests/web_api/test_trace.py | 7 +- tests/web_api/test_variables.py | 5 +- 106 files changed, 349 insertions(+), 304 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index f0e12deb7d..b0f4a97b5e 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,9 +1,8 @@ +from openfisca_core.types import Array, ArrayLike from typing import Any, Dict, Sequence, TypeVar import numpy -from openfisca_core.types import ArrayLike, Array - T = TypeVar("T") diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index ee985071bf..0966fa1dce 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,6 +1,5 @@ -from typing import TypeVar - from openfisca_core.types import Array +from typing import TypeVar T = TypeVar("T") diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index 5f5b5be02c..246fdfcd4c 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -1,9 +1,8 @@ +from openfisca_core.types import Array, ArrayLike from typing import Optional import numpy -from openfisca_core.types import ArrayLike, Array - def average_rate( target: Array[float], diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py index 94422913a1..dbf8a4eb13 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -4,8 +4,8 @@ import numpy from openfisca_core import periods -from openfisca_core.periods import DateUnit from openfisca_core.indexed_enums import EnumArray +from openfisca_core.periods import DateUnit class OnDiskStorage: diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index 15b38e2a5c..a081987049 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -21,7 +21,7 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .helpers import build_entity # noqa: F401 -from .role import Role # noqa: F401 from .entity import Entity # noqa: F401 from .group_entity import GroupEntity # noqa: F401 +from .helpers import build_entity # noqa: F401 +from .role import Role # noqa: F401 diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 62c803bd82..b0f9e2a0d4 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,10 +1,10 @@ +from openfisca_core.types import TaxBenefitSystem, Variable from typing import Any, Optional import os import textwrap -from openfisca_core.types import TaxBenefitSystem, Variable -from openfisca_core.entities import Role +from .role import Role class Entity: diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index 3f3a71599b..f6472d2c52 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,4 +1,5 @@ -from openfisca_core.entities import Entity, Role +from .entity import Entity +from .role import Role class GroupEntity(Entity): diff --git a/openfisca_core/errors/__init__.py b/openfisca_core/errors/__init__.py index 8be58e103a..41d4760bee 100644 --- a/openfisca_core/errors/__init__.py +++ b/openfisca_core/errors/__init__.py @@ -21,22 +21,34 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .cycle_error import CycleError # noqa: F401 -from .empty_argument_error import EmptyArgumentError # noqa: F401 -from .nan_creation_error import NaNCreationError # noqa: F401 -from .parameter_not_found_error import ( # noqa: F401 - ParameterNotFoundError, - ParameterNotFoundError as ParameterNotFound, -) -from .parameter_parsing_error import ParameterParsingError # noqa: F401 -from .period_mismatch_error import PeriodMismatchError # noqa: F401 -from .situation_parsing_error import SituationParsingError # noqa: F401 -from .spiral_error import SpiralError # noqa: F401 -from .variable_name_config_error import ( # noqa: F401 - VariableNameConflictError, +from .cycle_error import CycleError +from .empty_argument_error import EmptyArgumentError +from .nan_creation_error import NaNCreationError +from .parameter_not_found_error import ParameterNotFoundError +from .parameter_not_found_error import ParameterNotFoundError as ParameterNotFound +from .parameter_parsing_error import ParameterParsingError +from .period_mismatch_error import PeriodMismatchError +from .situation_parsing_error import SituationParsingError +from .spiral_error import SpiralError +from .variable_name_config_error import VariableNameConflictError +from .variable_name_config_error import ( VariableNameConflictError as VariableNameConflict, ) -from .variable_not_found_error import ( # noqa: F401 - VariableNotFoundError, - VariableNotFoundError as VariableNotFound, -) +from .variable_not_found_error import VariableNotFoundError +from .variable_not_found_error import VariableNotFoundError as VariableNotFound + +__all__ = [ + "CycleError", + "EmptyArgumentError", + "NaNCreationError", + "ParameterNotFound", # Deprecated alias for "ParameterNotFoundError + "ParameterNotFoundError", + "ParameterParsingError", + "PeriodMismatchError", + "SituationParsingError", + "SpiralError", + "VariableNameConflict", # Deprecated alias for "VariableNameConflictError" + "VariableNameConflictError", + "VariableNotFound", # Deprecated alias for "VariableNotFoundError" + "VariableNotFoundError", +] diff --git a/openfisca_core/errors/empty_argument_error.py b/openfisca_core/errors/empty_argument_error.py index ba22072e89..0d0205b432 100644 --- a/openfisca_core/errors/empty_argument_error.py +++ b/openfisca_core/errors/empty_argument_error.py @@ -1,6 +1,7 @@ +import typing + import os import traceback -import typing import numpy diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index fc0d6718bf..230c916d06 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -8,15 +8,11 @@ import numpy import psutil -from openfisca_core import ( - errors, - commons, - data_storage as storage, - indexed_enums as enums, - periods, - tools, - types, -) +from openfisca_core import commons +from openfisca_core import data_storage as storage +from openfisca_core import errors +from openfisca_core import indexed_enums as enums +from openfisca_core import periods, tools, types from .memory_usage import MemoryUsage diff --git a/openfisca_core/indexed_enums/__init__.py b/openfisca_core/indexed_enums/__init__.py index 6a18aa4809..874a7e1f9b 100644 --- a/openfisca_core/indexed_enums/__init__.py +++ b/openfisca_core/indexed_enums/__init__.py @@ -22,5 +22,5 @@ # See: https://www.python.org/dev/peps/pep-0008/#imports from .config import ENUM_ARRAY_DTYPE # noqa: F401 -from .enum_array import EnumArray # noqa: F401 from .enum import Enum # noqa: F401 +from .enum_array import EnumArray # noqa: F401 diff --git a/openfisca_core/indexed_enums/enum.py b/openfisca_core/indexed_enums/enum.py index 7382212567..7957ced3a2 100644 --- a/openfisca_core/indexed_enums/enum.py +++ b/openfisca_core/indexed_enums/enum.py @@ -1,11 +1,13 @@ from __future__ import annotations -import enum from typing import Union +import enum + import numpy -from . import ENUM_ARRAY_DTYPE, EnumArray +from .config import ENUM_ARRAY_DTYPE +from .enum_array import EnumArray class Enum(enum.Enum): diff --git a/openfisca_core/model_api.py b/openfisca_core/model_api.py index a2f5e34fa5..553ee75b34 100644 --- a/openfisca_core/model_api.py +++ b/openfisca_core/model_api.py @@ -1,39 +1,60 @@ -from datetime import date # noqa: F401 +from datetime import date -from numpy import ( # noqa: F401 - logical_not as not_, - maximum as max_, - minimum as min_, - round as round_, - select, - where, -) - -from openfisca_core.commons import apply_thresholds, concat, switch # noqa: F401 +from numpy import logical_not as not_ +from numpy import maximum as max_ +from numpy import minimum as min_ +from numpy import round as round_ +from numpy import select, where -from openfisca_core.holders import ( # noqa: F401 +from openfisca_core.commons import apply_thresholds, concat, switch +from openfisca_core.holders import ( set_input_dispatch_by_period, set_input_divide_by_period, ) - -from openfisca_core.indexed_enums import Enum # noqa: F401 - -from openfisca_core.parameters import ( # noqa: F401 - load_parameter_file, - ParameterNode, - Scale, +from openfisca_core.indexed_enums import Enum +from openfisca_core.parameters import ( Bracket, Parameter, + ParameterNode, + Scale, ValuesHistory, + load_parameter_file, ) - -from openfisca_core.periods import DAY, MONTH, YEAR, ETERNITY, period # noqa: F401 -from openfisca_core.populations import ADD, DIVIDE # noqa: F401 -from openfisca_core.reforms import Reform # noqa: F401 - -from openfisca_core.simulations import ( # noqa: F401 - calculate_output_add, - calculate_output_divide, -) - -from openfisca_core.variables import Variable # noqa: F401 +from openfisca_core.periods import DAY, ETERNITY, MONTH, YEAR, period +from openfisca_core.populations import ADD, DIVIDE +from openfisca_core.reforms import Reform +from openfisca_core.simulations import calculate_output_add, calculate_output_divide +from openfisca_core.variables import Variable + +__all__ = [ + "date", + "not_", + "max_", + "min_", + "round_", + "select", + "where", + "apply_thresholds", + "concat", + "switch", + "set_input_dispatch_by_period", + "set_input_divide_by_period", + "Enum", + "Bracket", + "Parameter", + "ParameterNode", + "Scale", + "ValuesHistory", + "load_parameter_file", + "DAY", + "ETERNITY", + "MONTH", + "YEAR", + "period", + "ADD", + "DIVIDE", + "Reform", + "calculate_output_add", + "calculate_output_divide", + "Variable", +] diff --git a/openfisca_core/parameters/__init__.py b/openfisca_core/parameters/__init__.py index e02d35c3ba..a5ac18044f 100644 --- a/openfisca_core/parameters/__init__.py +++ b/openfisca_core/parameters/__init__.py @@ -21,29 +21,47 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from openfisca_core.errors import ParameterNotFound, ParameterParsingError # noqa: F401 +from openfisca_core.errors import ParameterNotFound, ParameterParsingError - -from .config import ( # noqa: F401 +from .at_instant_like import AtInstantLike +from .config import ( ALLOWED_PARAM_TYPES, COMMON_KEYS, FILE_EXTENSIONS, date_constructor, dict_no_duplicate_constructor, ) +from .helpers import contains_nan, load_parameter_file +from .parameter import Parameter +from .parameter_at_instant import ParameterAtInstant +from .parameter_node import ParameterNode +from .parameter_node_at_instant import ParameterNodeAtInstant +from .parameter_scale import ParameterScale +from .parameter_scale import ParameterScale as Scale +from .parameter_scale_bracket import ParameterScaleBracket +from .parameter_scale_bracket import ParameterScaleBracket as Bracket +from .values_history import ValuesHistory +from .vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant -from .at_instant_like import AtInstantLike # noqa: F401 -from .helpers import contains_nan, load_parameter_file # noqa: F401 -from .parameter_at_instant import ParameterAtInstant # noqa: F401 -from .parameter_node_at_instant import ParameterNodeAtInstant # noqa: F401 -from .vectorial_parameter_node_at_instant import ( # noqa: F401 - VectorialParameterNodeAtInstant, -) -from .parameter import Parameter # noqa: F401 -from .parameter_node import ParameterNode # noqa: F401 -from .parameter_scale import ParameterScale, ParameterScale as Scale # noqa: F401 -from .parameter_scale_bracket import ( # noqa: F401 - ParameterScaleBracket, - ParameterScaleBracket as Bracket, -) -from .values_history import ValuesHistory # noqa: F401 +__all__ = [ + "ParameterNotFound", + "ParameterParsingError", + "AtInstantLike", + "ALLOWED_PARAM_TYPES", + "COMMON_KEYS", + "FILE_EXTENSIONS", + "date_constructor", + "dict_no_duplicate_constructor", + "contains_nan", + "load_parameter_file", + "Parameter", + "ParameterAtInstant", + "ParameterNode", + "ParameterNodeAtInstant", + "ParameterScale", + "Scale", + "ParameterScaleBracket", + "Bracket", + "ValuesHistory", + "VectorialParameterNodeAtInstant", +] diff --git a/openfisca_core/parameters/config.py b/openfisca_core/parameters/config.py index 6a0779a8ed..1900d0f550 100644 --- a/openfisca_core/parameters/config.py +++ b/openfisca_core/parameters/config.py @@ -1,9 +1,11 @@ -import warnings +import typing + import os +import warnings + import yaml -import typing -from openfisca_core.warnings import LibYAMLWarning +from openfisca_core.warnings import LibYAMLWarning try: from yaml import CLoader as Loader @@ -15,7 +17,9 @@ "so that it is used in your Python environment." + os.linesep, ] warnings.warn(" ".join(message), LibYAMLWarning, stacklevel=2) - from yaml import Loader # type: ignore # (see https://github.com/python/mypy/issues/1153#issuecomment-455802270) + from yaml import ( + Loader, # type: ignore # (see https://github.com/python/mypy/issues/1153#issuecomment-455802270) + ) # 'unit' and 'reference' are only listed here for backward compatibility. # It is now recommended to include them in metadata, until a common consensus emerges. diff --git a/openfisca_core/parameters/parameter.py b/openfisca_core/parameters/parameter.py index dcf59bf00c..9c9d7e7093 100644 --- a/openfisca_core/parameters/parameter.py +++ b/openfisca_core/parameters/parameter.py @@ -7,7 +7,10 @@ from openfisca_core import commons, periods from openfisca_core.errors import ParameterParsingError -from openfisca_core.parameters import config, helpers, AtInstantLike, ParameterAtInstant + +from . import config, helpers +from .at_instant_like import AtInstantLike +from .parameter_at_instant import ParameterAtInstant class Parameter(AtInstantLike): diff --git a/openfisca_core/parameters/parameter_at_instant.py b/openfisca_core/parameters/parameter_at_instant.py index edc7f54e8a..b84dc5b2b6 100644 --- a/openfisca_core/parameters/parameter_at_instant.py +++ b/openfisca_core/parameters/parameter_at_instant.py @@ -1,6 +1,7 @@ -import copy import typing +import copy + from openfisca_core import commons from openfisca_core.errors import ParameterParsingError from openfisca_core.parameters import config, helpers diff --git a/openfisca_core/parameters/parameter_node.py b/openfisca_core/parameters/parameter_node.py index 83a7d35d30..6a344a09a9 100644 --- a/openfisca_core/parameters/parameter_node.py +++ b/openfisca_core/parameters/parameter_node.py @@ -1,11 +1,16 @@ from __future__ import annotations +import typing + import copy import os -import typing from openfisca_core import commons, parameters, tools -from . import config, helpers, AtInstantLike, Parameter, ParameterNodeAtInstant + +from . import config, helpers +from .at_instant_like import AtInstantLike +from .parameter import Parameter +from .parameter_node_at_instant import ParameterNodeAtInstant class ParameterNode(AtInstantLike): diff --git a/openfisca_core/parameters/parameter_scale.py b/openfisca_core/parameters/parameter_scale.py index 8bfb8bd7b8..f3d636ed0c 100644 --- a/openfisca_core/parameters/parameter_scale.py +++ b/openfisca_core/parameters/parameter_scale.py @@ -1,10 +1,11 @@ +import typing + import copy import os -import typing from openfisca_core import commons, parameters, tools from openfisca_core.errors import ParameterParsingError -from openfisca_core.parameters import config, helpers, AtInstantLike +from openfisca_core.parameters import AtInstantLike, config, helpers from openfisca_core.taxscales import ( LinearAverageRateTaxScale, MarginalAmountTaxScale, diff --git a/openfisca_core/parameters/values_history.py b/openfisca_core/parameters/values_history.py index fc55400c89..4c56c72398 100644 --- a/openfisca_core/parameters/values_history.py +++ b/openfisca_core/parameters/values_history.py @@ -1,9 +1,5 @@ -from openfisca_core.parameters import Parameter +from .parameter import Parameter class ValuesHistory(Parameter): - """ - Only for backward compatibility. - """ - - pass + """Only for backward compatibility.""" diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index f7b901c58e..11a7b671b4 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,10 +1,10 @@ from __future__ import annotations import typing +from collections.abc import Sequence import calendar import datetime -from collections.abc import Sequence import pendulum diff --git a/openfisca_core/populations/__init__.py b/openfisca_core/populations/__init__.py index 25609c1683..e52a5bcaee 100644 --- a/openfisca_core/populations/__init__.py +++ b/openfisca_core/populations/__init__.py @@ -22,17 +22,16 @@ # See: https://www.python.org/dev/peps/pep-0008/#imports from openfisca_core.projectors import ( # noqa: F401 - Projector, EntityToPersonProjector, FirstPersonToEntityProjector, + Projector, UniqueRoleToEntityProjector, ) - from openfisca_core.projectors.helpers import ( # noqa: F401 - projectable, get_projector_from_shortcut, + projectable, ) from .config import ADD, DIVIDE # noqa: F401 -from .population import Population # noqa: F401 from .group_population import GroupPopulation # noqa: F401 +from .population import Population # noqa: F401 diff --git a/openfisca_core/populations/group_population.py b/openfisca_core/populations/group_population.py index 717f3e646c..3ec47fe291 100644 --- a/openfisca_core/populations/group_population.py +++ b/openfisca_core/populations/group_population.py @@ -5,7 +5,8 @@ from openfisca_core import projectors from openfisca_core.entities import Role from openfisca_core.indexed_enums import EnumArray -from openfisca_core.populations import Population + +from .population import Population class GroupPopulation(Population): diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index 5dac784aa4..4aa28b2ad9 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -1,5 +1,6 @@ from __future__ import annotations +from openfisca_core.types import Array, Entity, Period, Role, Simulation from typing import Dict, NamedTuple, Optional, Sequence, Union from typing_extensions import TypedDict @@ -10,7 +11,6 @@ from openfisca_core import periods, projectors from openfisca_core.holders import Holder, MemoryUsage from openfisca_core.projectors import Projector -from openfisca_core.types import Array, Entity, Period, Role, Simulation from . import config diff --git a/openfisca_core/projectors/__init__.py b/openfisca_core/projectors/__init__.py index 02982bf982..9711808d0d 100644 --- a/openfisca_core/projectors/__init__.py +++ b/openfisca_core/projectors/__init__.py @@ -21,8 +21,8 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .helpers import projectable, get_projector_from_shortcut # noqa: F401 -from .projector import Projector # noqa: F401 from .entity_to_person_projector import EntityToPersonProjector # noqa: F401 from .first_person_to_entity_projector import FirstPersonToEntityProjector # noqa: F401 +from .helpers import get_projector_from_shortcut, projectable # noqa: F401 +from .projector import Projector # noqa: F401 from .unique_role_to_entity_projector import UniqueRoleToEntityProjector # noqa: F401 diff --git a/openfisca_core/projectors/entity_to_person_projector.py b/openfisca_core/projectors/entity_to_person_projector.py index dca3f2df94..ca6245a1f7 100644 --- a/openfisca_core/projectors/entity_to_person_projector.py +++ b/openfisca_core/projectors/entity_to_person_projector.py @@ -1,4 +1,4 @@ -from openfisca_core.projectors import Projector +from .projector import Projector class EntityToPersonProjector(Projector): diff --git a/openfisca_core/projectors/first_person_to_entity_projector.py b/openfisca_core/projectors/first_person_to_entity_projector.py index 4a76cd1cf8..4b4e7b7994 100644 --- a/openfisca_core/projectors/first_person_to_entity_projector.py +++ b/openfisca_core/projectors/first_person_to_entity_projector.py @@ -1,4 +1,4 @@ -from openfisca_core.projectors import Projector +from .projector import Projector class FirstPersonToEntityProjector(Projector): diff --git a/openfisca_core/projectors/unique_role_to_entity_projector.py b/openfisca_core/projectors/unique_role_to_entity_projector.py index 6f7cce3757..fed2f249ca 100644 --- a/openfisca_core/projectors/unique_role_to_entity_projector.py +++ b/openfisca_core/projectors/unique_role_to_entity_projector.py @@ -1,4 +1,4 @@ -from openfisca_core.projectors import Projector +from .projector import Projector class UniqueRoleToEntityProjector(Projector): diff --git a/openfisca_core/scripts/__init__.py b/openfisca_core/scripts/__init__.py index e673fa75bb..6366c8df15 100644 --- a/openfisca_core/scripts/__init__.py +++ b/openfisca_core/scripts/__init__.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -import traceback import importlib import logging import pkgutil +import traceback from os import linesep log = logging.getLogger(__name__) diff --git a/openfisca_core/scripts/find_placeholders.py b/openfisca_core/scripts/find_placeholders.py index 2cd31c3cf8..37f31f6727 100644 --- a/openfisca_core/scripts/find_placeholders.py +++ b/openfisca_core/scripts/find_placeholders.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # flake8: noqa T001 -import os import fnmatch +import os import sys from bs4 import BeautifulSoup diff --git a/openfisca_core/scripts/measure_numpy_condition_notations.py b/openfisca_core/scripts/measure_numpy_condition_notations.py index 2f37e816e4..f737413bf4 100755 --- a/openfisca_core/scripts/measure_numpy_condition_notations.py +++ b/openfisca_core/scripts/measure_numpy_condition_notations.py @@ -11,14 +11,13 @@ The aim of this script is to compare the time taken by the calculation of the values """ -from contextlib import contextmanager import argparse import sys import time +from contextlib import contextmanager import numpy - args = None diff --git a/openfisca_core/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index 75125b8863..89fd47b441 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -13,12 +13,11 @@ from numpy.core.defchararray import startswith from openfisca_core import periods, simulations -from openfisca_core.periods import DateUnit from openfisca_core.entities import build_entity -from openfisca_core.variables import Variable +from openfisca_core.periods import DateUnit from openfisca_core.taxbenefitsystems import TaxBenefitSystem from openfisca_core.tools import assert_near - +from openfisca_core.variables import Variable args = None diff --git a/openfisca_core/scripts/measure_performances_fancy_indexing.py b/openfisca_core/scripts/measure_performances_fancy_indexing.py index b72f436033..030b1af7aa 100644 --- a/openfisca_core/scripts/measure_performances_fancy_indexing.py +++ b/openfisca_core/scripts/measure_performances_fancy_indexing.py @@ -3,10 +3,8 @@ import timeit import numpy as np - from openfisca_france import CountryTaxBenefitSystem - tbs = CountryTaxBenefitSystem() N = 200000 al_plaf_acc = tbs.get_parameters_at_instant("2015-01-01").prestations.al_plaf_acc diff --git a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py index d3cb44dc59..3ff9c3d7ac 100644 --- a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +++ b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py @@ -7,10 +7,11 @@ or just (output is written in a directory called `yaml_parameters`): `python xml_to_yaml_country_template.py` """ -import sys import os +import sys + +from openfisca_country_template import COUNTRY_DIR, CountryTaxBenefitSystem -from openfisca_country_template import CountryTaxBenefitSystem, COUNTRY_DIR from . import xml_to_yaml tax_benefit_system = CountryTaxBenefitSystem() diff --git a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py index b2c113268d..34b7ca430d 100644 --- a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +++ b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py @@ -8,12 +8,13 @@ `python xml_to_yaml_extension_template.py` """ -import sys import os +import sys -from . import xml_to_yaml import openfisca_extension_template +from . import xml_to_yaml + if len(sys.argv) > 1: target_path = sys.argv[1] else: diff --git a/openfisca_core/scripts/migrations/v24_to_25.py b/openfisca_core/scripts/migrations/v24_to_25.py index ce364ef994..1eefd426ad 100644 --- a/openfisca_core/scripts/migrations/v24_to_25.py +++ b/openfisca_core/scripts/migrations/v24_to_25.py @@ -2,9 +2,10 @@ # flake8: noqa T001 import argparse -import os import glob +import os +from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedSeq from openfisca_core.scripts import ( @@ -12,8 +13,6 @@ build_tax_benefit_system, ) -from ruamel.yaml import YAML - yaml = YAML() yaml.default_flow_style = False yaml.width = 4096 diff --git a/openfisca_core/scripts/openfisca_command.py b/openfisca_core/scripts/openfisca_command.py index 441483ecd6..3b835e73a3 100644 --- a/openfisca_core/scripts/openfisca_command.py +++ b/openfisca_core/scripts/openfisca_command.py @@ -1,6 +1,6 @@ import argparse -import warnings import sys +import warnings from openfisca_core.scripts import add_tax_benefit_system_arguments diff --git a/openfisca_core/scripts/remove_fuzzy.py b/openfisca_core/scripts/remove_fuzzy.py index 05675ea75e..2c06b149b1 100755 --- a/openfisca_core/scripts/remove_fuzzy.py +++ b/openfisca_core/scripts/remove_fuzzy.py @@ -1,9 +1,10 @@ # remove_fuzzy.py : Remove the fuzzy attribute in xml files and add END tags. # See https://github.com/openfisca/openfisca-core/issues/437 -import re import datetime +import re import sys + import numpy assert len(sys.argv) == 2 diff --git a/openfisca_core/scripts/run_test.py b/openfisca_core/scripts/run_test.py index f9ca4d3349..ab292c4165 100644 --- a/openfisca_core/scripts/run_test.py +++ b/openfisca_core/scripts/run_test.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- import logging -import sys import os +import sys -from openfisca_core.tools.test_runner import run_tests from openfisca_core.scripts import build_tax_benefit_system +from openfisca_core.tools.test_runner import run_tests def main(parser): diff --git a/openfisca_core/scripts/simulation_generator.py b/openfisca_core/scripts/simulation_generator.py index 5e451c4e0f..4f2c5bfd4f 100644 --- a/openfisca_core/scripts/simulation_generator.py +++ b/openfisca_core/scripts/simulation_generator.py @@ -1,6 +1,7 @@ +import random + import numpy -import random from openfisca_core.simulations import Simulation diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 95c90a9ee4..d501c4f915 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,5 +1,6 @@ from __future__ import annotations +from openfisca_core.types import Population, TaxBenefitSystem, Variable from typing import Dict, NamedTuple, Optional, Set import tempfile @@ -16,7 +17,6 @@ SimpleTracer, TracingParameterNodeAtInstant, ) -from openfisca_core.types import Population, TaxBenefitSystem, Variable from openfisca_core.warnings import TempfileWarning diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 41ca1e22e3..71c37a3524 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -1,8 +1,8 @@ -from typing import Dict, List, Iterable +from typing import Dict, Iterable, List import copy -import dpath.util +import dpath.util import numpy from openfisca_core import periods @@ -13,7 +13,7 @@ VariableNotFoundError, ) from openfisca_core.populations import Population -from openfisca_core.simulations import helpers, Simulation +from openfisca_core.simulations import Simulation, helpers from openfisca_core.variables import Variable diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 812448ae1d..f03ecb0571 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -1,5 +1,7 @@ from __future__ import annotations +import typing +from openfisca_core.types import ParameterNodeAtInstant from typing import Any, Dict, Optional, Sequence, Union import ast @@ -7,23 +9,22 @@ import functools import glob import importlib -import importlib_metadata import inspect +import linecache import logging import os import sys import traceback -import typing -import linecache + +import importlib_metadata from openfisca_core import commons, periods, variables from openfisca_core.entities import Entity from openfisca_core.errors import VariableNameConflictError, VariableNotFoundError from openfisca_core.parameters import ParameterNode from openfisca_core.periods import Instant, Period -from openfisca_core.populations import Population, GroupPopulation +from openfisca_core.populations import GroupPopulation, Population from openfisca_core.simulations import SimulationBuilder -from openfisca_core.types import ParameterNodeAtInstant from openfisca_core.variables import Variable log = logging.getLogger(__name__) diff --git a/openfisca_core/taxscales/__init__.py b/openfisca_core/taxscales/__init__.py index 0364101d71..1911d20c56 100644 --- a/openfisca_core/taxscales/__init__.py +++ b/openfisca_core/taxscales/__init__.py @@ -23,13 +23,13 @@ from openfisca_core.errors import EmptyArgumentError # noqa: F401 -from .helpers import combine_tax_scales # noqa: F401 -from .tax_scale_like import TaxScaleLike # noqa: F401 -from .rate_tax_scale_like import RateTaxScaleLike # noqa: F401 -from .marginal_rate_tax_scale import MarginalRateTaxScale # noqa: F401 -from .linear_average_rate_tax_scale import LinearAverageRateTaxScale # noqa: F401 +from .abstract_rate_tax_scale import AbstractRateTaxScale # noqa: F401 from .abstract_tax_scale import AbstractTaxScale # noqa: F401 from .amount_tax_scale_like import AmountTaxScaleLike # noqa: F401 -from .abstract_rate_tax_scale import AbstractRateTaxScale # noqa: F401 +from .helpers import combine_tax_scales # noqa: F401 +from .linear_average_rate_tax_scale import LinearAverageRateTaxScale # noqa: F401 from .marginal_amount_tax_scale import MarginalAmountTaxScale # noqa: F401 +from .marginal_rate_tax_scale import MarginalRateTaxScale # noqa: F401 +from .rate_tax_scale_like import RateTaxScaleLike # noqa: F401 from .single_amount_tax_scale import SingleAmountTaxScale # noqa: F401 +from .tax_scale_like import TaxScaleLike # noqa: F401 diff --git a/openfisca_core/taxscales/abstract_rate_tax_scale.py b/openfisca_core/taxscales/abstract_rate_tax_scale.py index ecc17c7a66..cd04ba872e 100644 --- a/openfisca_core/taxscales/abstract_rate_tax_scale.py +++ b/openfisca_core/taxscales/abstract_rate_tax_scale.py @@ -1,9 +1,10 @@ from __future__ import annotations import typing + import warnings -from openfisca_core.taxscales import RateTaxScaleLike +from .rate_tax_scale_like import RateTaxScaleLike if typing.TYPE_CHECKING: import numpy diff --git a/openfisca_core/taxscales/abstract_tax_scale.py b/openfisca_core/taxscales/abstract_tax_scale.py index 8fbed393a5..43b21f8141 100644 --- a/openfisca_core/taxscales/abstract_tax_scale.py +++ b/openfisca_core/taxscales/abstract_tax_scale.py @@ -1,9 +1,10 @@ from __future__ import annotations import typing + import warnings -from openfisca_core.taxscales import TaxScaleLike +from .tax_scale_like import TaxScaleLike if typing.TYPE_CHECKING: import numpy diff --git a/openfisca_core/taxscales/amount_tax_scale_like.py b/openfisca_core/taxscales/amount_tax_scale_like.py index f7fb70cb3d..7cdfd4cb34 100644 --- a/openfisca_core/taxscales/amount_tax_scale_like.py +++ b/openfisca_core/taxscales/amount_tax_scale_like.py @@ -1,10 +1,12 @@ import abc +import typing + import bisect import os -import typing from openfisca_core import tools -from openfisca_core.taxscales import TaxScaleLike + +from .tax_scale_like import TaxScaleLike class AmountTaxScaleLike(TaxScaleLike, abc.ABC): diff --git a/openfisca_core/taxscales/helpers.py b/openfisca_core/taxscales/helpers.py index a09420d098..62ee431be9 100644 --- a/openfisca_core/taxscales/helpers.py +++ b/openfisca_core/taxscales/helpers.py @@ -1,8 +1,9 @@ from __future__ import annotations -import logging import typing +import logging + from openfisca_core import taxscales log = logging.getLogger(__name__) diff --git a/openfisca_core/taxscales/linear_average_rate_tax_scale.py b/openfisca_core/taxscales/linear_average_rate_tax_scale.py index 591e53de56..60b2053d5d 100644 --- a/openfisca_core/taxscales/linear_average_rate_tax_scale.py +++ b/openfisca_core/taxscales/linear_average_rate_tax_scale.py @@ -1,12 +1,14 @@ from __future__ import annotations -import logging import typing +import logging + import numpy from openfisca_core import taxscales -from openfisca_core.taxscales import RateTaxScaleLike + +from .rate_tax_scale_like import RateTaxScaleLike log = logging.getLogger(__name__) diff --git a/openfisca_core/taxscales/marginal_amount_tax_scale.py b/openfisca_core/taxscales/marginal_amount_tax_scale.py index d11c6090c8..fa8a0897f7 100644 --- a/openfisca_core/taxscales/marginal_amount_tax_scale.py +++ b/openfisca_core/taxscales/marginal_amount_tax_scale.py @@ -4,7 +4,7 @@ import numpy -from openfisca_core.taxscales import AmountTaxScaleLike +from .amount_tax_scale_like import AmountTaxScaleLike if typing.TYPE_CHECKING: NumericalArray = typing.Union[numpy.int_, numpy.float_] diff --git a/openfisca_core/taxscales/marginal_rate_tax_scale.py b/openfisca_core/taxscales/marginal_rate_tax_scale.py index 6e7d94da7b..2604c156e1 100644 --- a/openfisca_core/taxscales/marginal_rate_tax_scale.py +++ b/openfisca_core/taxscales/marginal_rate_tax_scale.py @@ -1,13 +1,15 @@ from __future__ import annotations +import typing + import bisect import itertools -import typing import numpy from openfisca_core import taxscales -from openfisca_core.taxscales import RateTaxScaleLike + +from .rate_tax_scale_like import RateTaxScaleLike if typing.TYPE_CHECKING: NumericalArray = typing.Union[numpy.int_, numpy.float_] diff --git a/openfisca_core/taxscales/rate_tax_scale_like.py b/openfisca_core/taxscales/rate_tax_scale_like.py index e80b4ecb87..b890eb6801 100644 --- a/openfisca_core/taxscales/rate_tax_scale_like.py +++ b/openfisca_core/taxscales/rate_tax_scale_like.py @@ -1,15 +1,17 @@ from __future__ import annotations import abc +import typing + import bisect import os -import typing import numpy from openfisca_core import tools from openfisca_core.errors import EmptyArgumentError -from openfisca_core.taxscales import TaxScaleLike + +from .tax_scale_like import TaxScaleLike if typing.TYPE_CHECKING: NumericalArray = typing.Union[numpy.int_, numpy.float_] diff --git a/openfisca_core/taxscales/tax_scale_like.py b/openfisca_core/taxscales/tax_scale_like.py index 99b7887f67..c0c1a8adb9 100644 --- a/openfisca_core/taxscales/tax_scale_like.py +++ b/openfisca_core/taxscales/tax_scale_like.py @@ -1,9 +1,10 @@ from __future__ import annotations import abc -import copy import typing +import copy + import numpy from openfisca_core import commons diff --git a/openfisca_core/tools/simulation_dumper.py b/openfisca_core/tools/simulation_dumper.py index b33dd9b4e9..ab21bd79f0 100644 --- a/openfisca_core/tools/simulation_dumper.py +++ b/openfisca_core/tools/simulation_dumper.py @@ -5,9 +5,9 @@ import numpy as np -from openfisca_core.simulations import Simulation from openfisca_core.data_storage import OnDiskStorage from openfisca_core.periods import DateUnit +from openfisca_core.simulations import Simulation def dump_simulation(simulation, directory): diff --git a/openfisca_core/tools/test_runner.py b/openfisca_core/tools/test_runner.py index c70d3266de..21987068e1 100644 --- a/openfisca_core/tools/test_runner.py +++ b/openfisca_core/tools/test_runner.py @@ -1,5 +1,6 @@ from __future__ import annotations +from openfisca_core.types import TaxBenefitSystem from typing import Any, Dict, Optional, Sequence, Union from typing_extensions import Literal, TypedDict @@ -16,7 +17,6 @@ from openfisca_core.errors import SituationParsingError, VariableNotFound from openfisca_core.simulation_builder import SimulationBuilder from openfisca_core.tools import assert_near -from openfisca_core.types import TaxBenefitSystem from openfisca_core.warnings import LibYAMLWarning diff --git a/openfisca_core/tracers/computation_log.py b/openfisca_core/tracers/computation_log.py index a05a484ba2..1013b828f2 100644 --- a/openfisca_core/tracers/computation_log.py +++ b/openfisca_core/tracers/computation_log.py @@ -5,9 +5,10 @@ import numpy -from .. import tracers from openfisca_core.indexed_enums import EnumArray +from .. import tracers + if typing.TYPE_CHECKING: from numpy.typing import ArrayLike diff --git a/openfisca_core/tracers/full_tracer.py b/openfisca_core/tracers/full_tracer.py index 7be8458622..ee127792b3 100644 --- a/openfisca_core/tracers/full_tracer.py +++ b/openfisca_core/tracers/full_tracer.py @@ -1,9 +1,10 @@ from __future__ import annotations -import time import typing from typing import Dict, Iterator, List, Optional, Union +import time + from .. import tracers if typing.TYPE_CHECKING: diff --git a/openfisca_core/tracers/performance_log.py b/openfisca_core/tracers/performance_log.py index 565a4383fb..89917dc50e 100644 --- a/openfisca_core/tracers/performance_log.py +++ b/openfisca_core/tracers/performance_log.py @@ -1,11 +1,12 @@ from __future__ import annotations +import typing + import csv import importlib.resources import itertools import json import os -import typing from .. import tracers diff --git a/openfisca_core/tracers/trace_node.py b/openfisca_core/tracers/trace_node.py index 70c69c101e..4e0cceae0a 100644 --- a/openfisca_core/tracers/trace_node.py +++ b/openfisca_core/tracers/trace_node.py @@ -1,8 +1,9 @@ from __future__ import annotations -import dataclasses import typing +import dataclasses + if typing.TYPE_CHECKING: import numpy diff --git a/openfisca_core/types/__init__.py b/openfisca_core/types/__init__.py index 57214be7b5..eb403c46c9 100644 --- a/openfisca_core/types/__init__.py +++ b/openfisca_core/types/__init__.py @@ -49,11 +49,7 @@ # Official Public API -from ._data import ( # noqa: F401 - Array, - ArrayLike, -) - +from ._data import Array, ArrayLike # noqa: F401 from ._domain import ( # noqa: F401 Entity, Formula, diff --git a/openfisca_core/types/_data.py b/openfisca_core/types/_data.py index 96a729ddca..928e1b9174 100644 --- a/openfisca_core/types/_data.py +++ b/openfisca_core/types/_data.py @@ -1,8 +1,7 @@ # from typing import Sequence, TypeVar, Union -from typing import Sequence, TypeVar - # from nptyping import types, NDArray as Array from numpy.typing import NDArray as Array # noqa: F401 +from typing import Sequence, TypeVar # import numpy diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types/_domain.py index bce6bb9aa0..8b9022d418 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types/_domain.py @@ -1,7 +1,6 @@ from __future__ import annotations import abc - import typing_extensions from typing import Any, Optional from typing_extensions import Protocol diff --git a/openfisca_core/variables/__init__.py b/openfisca_core/variables/__init__.py index 3decaf8f42..1ab191c5ce 100644 --- a/openfisca_core/variables/__init__.py +++ b/openfisca_core/variables/__init__.py @@ -21,6 +21,6 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .config import VALUE_TYPES, FORMULA_NAME_PREFIX # noqa: F401 +from .config import FORMULA_NAME_PREFIX, VALUE_TYPES # noqa: F401 from .helpers import get_annualized_variable, get_neutralized_variable # noqa: F401 from .variable import Variable # noqa: F401 diff --git a/openfisca_core/variables/config.py b/openfisca_core/variables/config.py index 81a09c4ecc..54270145bf 100644 --- a/openfisca_core/variables/config.py +++ b/openfisca_core/variables/config.py @@ -5,7 +5,6 @@ from openfisca_core import indexed_enums from openfisca_core.indexed_enums import Enum - VALUE_TYPES = { bool: { "dtype": numpy.bool_, diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index ee36d7a303..ce1eede9fc 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -1,8 +1,9 @@ from __future__ import annotations -import sortedcontainers from typing import Optional +import sortedcontainers + from openfisca_core.periods import Period from .. import variables diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 33cae3b20d..7fb70dee61 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -1,5 +1,6 @@ from __future__ import annotations +from openfisca_core.types import Formula, Instant from typing import Optional, Union import datetime @@ -13,7 +14,6 @@ from openfisca_core.entities import Entity from openfisca_core.indexed_enums import Enum, EnumArray from openfisca_core.periods import DateUnit, Period -from openfisca_core.types import Formula, Instant from . import config, helpers diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 0494b2056e..69a1a50957 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -58,6 +58,6 @@ lint-typing-strict-%: ## Run code formatters to correct style errors. format-style: $(shell git ls-files "*.py") @$(call print_help,$@:) - @isort openfisca_core/periods + @isort $? @black $? @$(call print_pass,$@:) diff --git a/openfisca_web_api/app.py b/openfisca_web_api/app.py index 4d54207a55..b4682b17e7 100644 --- a/openfisca_web_api/app.py +++ b/openfisca_web_api/app.py @@ -4,16 +4,16 @@ import os import traceback -from openfisca_core.errors import SituationParsingError, PeriodMismatchError -from openfisca_web_api.loader import build_data -from openfisca_web_api.errors import handle_import_error +from openfisca_core.errors import PeriodMismatchError, SituationParsingError from openfisca_web_api import handlers +from openfisca_web_api.errors import handle_import_error +from openfisca_web_api.loader import build_data try: - from flask import Flask, jsonify, abort, request, make_response, redirect + import werkzeug.exceptions + from flask import Flask, abort, jsonify, make_response, redirect, request from flask_cors import CORS from werkzeug.middleware.proxy_fix import ProxyFix - import werkzeug.exceptions except ImportError as error: handle_import_error(error) diff --git a/openfisca_web_api/handlers.py b/openfisca_web_api/handlers.py index 47b99338a3..a336a490b0 100644 --- a/openfisca_web_api/handlers.py +++ b/openfisca_web_api/handlers.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- import dpath.util -from openfisca_core.simulation_builder import SimulationBuilder + from openfisca_core.indexed_enums import Enum +from openfisca_core.simulation_builder import SimulationBuilder def calculate(tax_benefit_system, input_data: dict) -> dict: diff --git a/openfisca_web_api/loader/__init__.py b/openfisca_web_api/loader/__init__.py index 169bc58b4e..c62831c36d 100644 --- a/openfisca_web_api/loader/__init__.py +++ b/openfisca_web_api/loader/__init__.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from openfisca_web_api.loader.parameters import build_parameters -from openfisca_web_api.loader.variables import build_variables from openfisca_web_api.loader.entities import build_entities +from openfisca_web_api.loader.parameters import build_parameters from openfisca_web_api.loader.spec import build_openAPI_specification +from openfisca_web_api.loader.variables import build_variables def build_data(tax_benefit_system): diff --git a/openfisca_web_api/loader/spec.py b/openfisca_web_api/loader/spec.py index 47948c02bb..335317d2fe 100644 --- a/openfisca_web_api/loader/spec.py +++ b/openfisca_web_api/loader/spec.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- import os -import yaml from copy import deepcopy import dpath.util +import yaml from openfisca_core.indexed_enums import Enum from openfisca_web_api import handlers - OPEN_API_CONFIG_FILE = os.path.join( os.path.dirname(os.path.abspath(__file__)), os.path.pardir, "openAPI.yml" ) diff --git a/openfisca_web_api/loader/tax_benefit_system.py b/openfisca_web_api/loader/tax_benefit_system.py index 53a2c47107..3cbd0edb81 100644 --- a/openfisca_web_api/loader/tax_benefit_system.py +++ b/openfisca_web_api/loader/tax_benefit_system.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- import importlib -import traceback import logging +import traceback from os import linesep log = logging.getLogger(__name__) diff --git a/openfisca_web_api/scripts/serve.py b/openfisca_web_api/scripts/serve.py index 7522175059..ea594d9d6c 100644 --- a/openfisca_web_api/scripts/serve.py +++ b/openfisca_web_api/scripts/serve.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -import sys import logging +import sys from openfisca_core.scripts import build_tax_benefit_system from openfisca_web_api.app import create_app from openfisca_web_api.errors import handle_import_error try: - from gunicorn.app.base import BaseApplication from gunicorn import config + from gunicorn.app.base import BaseApplication except ImportError as error: handle_import_error(error) diff --git a/setup.cfg b/setup.cfg index ac338f8558..9740d15ff8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,29 +1,19 @@ # C011X: We (progressively) document the code base. # D10X: We (progressively) check docstrings (see https://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping). # DARXXX: We (progressively) check docstrings (see https://github.com/terrencepreilly/darglint#error-codes). -# 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. +# E203: We ignore a false positive in whitespace before ":" (see https://github.com/PyCQA/pycodestyle/issues/373). # F403/405: We ignore * imports. # R0401: We avoid cyclic imports —required for unit/doc tests. -# RST301: We use Google Python Style (see https://pypi.org/project/flake8-rst-docstrings/) +# RST301: We use Google Python Style (see https://pypi.org/project/flake8-rst-docstrings/). # W503/504: We break lines before binary operators (Knuth's style). [flake8] -extend-ignore = - D, - # See https://github.com/PyCQA/pycodestyle/issues/373 - E203, - E501, - F405, - W503 -# hang-closing = true -# ignore = E128,E251,F403,F405,E501,RST301,W503,W504 +extend-ignore = D +ignore = E203, E501, F405, RST301, W503 in-place = true include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/types -# Recommend matching the black line length (default 88), -# rather than using the flake8 default of 79: max-line-length = 88 +per-file-ignores = */typing.py:D101,D102,E704, */__init__.py:F401 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, mod, obj strictness = short @@ -34,16 +24,16 @@ enable = C0115,C0116,R0401 score = no [isort] -case_sensitive = true +case_sensitive = true force_alphabetical_sort_within_sections = false -group_by_package = true +group_by_package = true include_trailing_comma = true -known_first_party = openfisca_core -known_third_party = openfisca_country_template,openfisca_extension_template -known_typing = mypy,mypy_extensions,nptyping,types,typing,typing_extensions -multi_line_output = 8 -py_version = 37 -sections = FUTURE,TYPING,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +known_first_party = openfisca_core +known_openfisca = openfisca_country_template, openfisca_extension_template +known_typing = *abc*, *mypy*, *types*, *typing* +profile = black +py_version = 39 +sections = FUTURE, TYPING, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER [coverage:paths] source = . */site-packages diff --git a/setup.py b/setup.py index 15e58543ce..3d77867b1f 100644 --- a/setup.py +++ b/setup.py @@ -15,9 +15,10 @@ """ -from setuptools import setup, find_packages from pathlib import Path +from setuptools import find_packages, setup + # Read the contents of our README file for PyPi this_directory = Path(__file__).parent long_description = (this_directory / "README.md").read_text() diff --git a/tests/core/parameter_validation/test_parameter_validation.py b/tests/core/parameter_validation/test_parameter_validation.py index 6b47a8b495..f4b8a82d50 100644 --- a/tests/core/parameter_validation/test_parameter_validation.py +++ b/tests/core/parameter_validation/test_parameter_validation.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- import os + import pytest + from openfisca_core.parameters import ( - load_parameter_file, ParameterNode, ParameterParsingError, + load_parameter_file, ) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py index 4d682680c4..73a4ccb323 100644 --- a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py +++ b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py @@ -6,12 +6,10 @@ import numpy as np import pytest - -from openfisca_core.parameters import ParameterNode, Parameter, ParameterNotFound from openfisca_core.indexed_enums import Enum +from openfisca_core.parameters import Parameter, ParameterNode, ParameterNotFound from openfisca_core.tools import assert_near - LOCAL_DIR = os.path.dirname(os.path.abspath(__file__)) parameters = ParameterNode(directory_path=LOCAL_DIR) diff --git a/tests/core/tax_scales/test_abstract_rate_tax_scale.py b/tests/core/tax_scales/test_abstract_rate_tax_scale.py index 3d906dfcb4..ad755075be 100644 --- a/tests/core/tax_scales/test_abstract_rate_tax_scale.py +++ b/tests/core/tax_scales/test_abstract_rate_tax_scale.py @@ -1,7 +1,7 @@ -from openfisca_core import taxscales - import pytest +from openfisca_core import taxscales + def test_abstract_tax_scale(): with pytest.warns(DeprecationWarning): diff --git a/tests/core/tax_scales/test_abstract_tax_scale.py b/tests/core/tax_scales/test_abstract_tax_scale.py index 7746ea03ae..f1bfc4e4af 100644 --- a/tests/core/tax_scales/test_abstract_tax_scale.py +++ b/tests/core/tax_scales/test_abstract_tax_scale.py @@ -1,7 +1,7 @@ -from openfisca_core import taxscales - import pytest +from openfisca_core import taxscales + def test_abstract_tax_scale(): with pytest.warns(DeprecationWarning): diff --git a/tests/core/tax_scales/test_linear_average_rate_tax_scale.py b/tests/core/tax_scales/test_linear_average_rate_tax_scale.py index 9f7216dd4d..18e6bd5a4a 100644 --- a/tests/core/tax_scales/test_linear_average_rate_tax_scale.py +++ b/tests/core/tax_scales/test_linear_average_rate_tax_scale.py @@ -1,10 +1,8 @@ import numpy - -from openfisca_core import taxscales -from openfisca_core import tools - import pytest +from openfisca_core import taxscales, tools + def test_bracket_indices(): tax_base = numpy.array([0, 1, 2, 3, 4, 5]) diff --git a/tests/core/tax_scales/test_marginal_amount_tax_scale.py b/tests/core/tax_scales/test_marginal_amount_tax_scale.py index 685c527f36..e00a8371c4 100644 --- a/tests/core/tax_scales/test_marginal_amount_tax_scale.py +++ b/tests/core/tax_scales/test_marginal_amount_tax_scale.py @@ -1,12 +1,8 @@ from numpy import array - -from openfisca_core import parameters -from openfisca_core import periods -from openfisca_core import taxscales -from openfisca_core import tools - from pytest import fixture +from openfisca_core import parameters, periods, taxscales, tools + @fixture def data(): diff --git a/tests/core/tax_scales/test_marginal_rate_tax_scale.py b/tests/core/tax_scales/test_marginal_rate_tax_scale.py index 488b84214f..3ed4a3f12f 100644 --- a/tests/core/tax_scales/test_marginal_rate_tax_scale.py +++ b/tests/core/tax_scales/test_marginal_rate_tax_scale.py @@ -1,10 +1,8 @@ import numpy - -from openfisca_core import taxscales -from openfisca_core import tools - import pytest +from openfisca_core import taxscales, tools + def test_bracket_indices(): tax_base = numpy.array([0, 1, 2, 3, 4, 5]) diff --git a/tests/core/tax_scales/test_single_amount_tax_scale.py b/tests/core/tax_scales/test_single_amount_tax_scale.py index bd6682a993..ffcd32e092 100644 --- a/tests/core/tax_scales/test_single_amount_tax_scale.py +++ b/tests/core/tax_scales/test_single_amount_tax_scale.py @@ -1,12 +1,8 @@ import numpy - -from openfisca_core import parameters -from openfisca_core import periods -from openfisca_core import taxscales -from openfisca_core import tools - from pytest import fixture +from openfisca_core import parameters, periods, taxscales, tools + @fixture def data(): diff --git a/tests/core/tax_scales/test_tax_scales_commons.py b/tests/core/tax_scales/test_tax_scales_commons.py index cf14f10f18..e4426cd49d 100644 --- a/tests/core/tax_scales/test_tax_scales_commons.py +++ b/tests/core/tax_scales/test_tax_scales_commons.py @@ -1,9 +1,7 @@ -from openfisca_core import parameters -from openfisca_core import taxscales -from openfisca_core import tools - import pytest +from openfisca_core import parameters, taxscales, tools + @pytest.fixture def node(): diff --git a/tests/core/test_axes.py b/tests/core/test_axes.py index 5d2390e135..eb0f58caac 100644 --- a/tests/core/test_axes.py +++ b/tests/core/test_axes.py @@ -3,7 +3,6 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.tools import test_runner - # With periods diff --git a/tests/core/test_formulas.py b/tests/core/test_formulas.py index e45b93d3ef..c8a5379801 100644 --- a/tests/core/test_formulas.py +++ b/tests/core/test_formulas.py @@ -1,4 +1,5 @@ import numpy +from pytest import approx, fixture from openfisca_country_template import entities @@ -7,8 +8,6 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable -from pytest import fixture, approx - class choice(Variable): value_type = int @@ -109,9 +108,9 @@ def test_group_encapsulation(): And calculations are projected to all the member families. """ - from openfisca_core.taxbenefitsystems import TaxBenefitSystem from openfisca_core.entities import build_entity from openfisca_core.periods import DateUnit + from openfisca_core.taxbenefitsystems import TaxBenefitSystem person_entity = build_entity( key="person", diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index 0d4f30fe4d..088ca15935 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -1,16 +1,15 @@ -import pytest - import numpy +import pytest from openfisca_country_template import situation_examples from openfisca_country_template.variables import housing from openfisca_core import holders, periods, tools from openfisca_core.errors import PeriodMismatchError +from openfisca_core.holders import Holder from openfisca_core.memory_config import MemoryConfig from openfisca_core.periods import DateUnit from openfisca_core.simulations import SimulationBuilder -from openfisca_core.holders import Holder @pytest.fixture diff --git a/tests/core/test_opt_out_cache.py b/tests/core/test_opt_out_cache.py index 01efb315bf..e9fe3a2469 100644 --- a/tests/core/test_opt_out_cache.py +++ b/tests/core/test_opt_out_cache.py @@ -6,7 +6,6 @@ from openfisca_core.periods import DateUnit from openfisca_core.variables import Variable - PERIOD = periods.period("2016-01") diff --git a/tests/core/test_parameters.py b/tests/core/test_parameters.py index 4fca66ed43..13e2874787 100644 --- a/tests/core/test_parameters.py +++ b/tests/core/test_parameters.py @@ -3,9 +3,9 @@ import pytest from openfisca_core.parameters import ( - ParameterNotFound, ParameterNode, ParameterNodeAtInstant, + ParameterNotFound, load_parameter_file, ) diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index 5d2a08e816..0c17bb1169 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -5,9 +5,8 @@ from openfisca_country_template.entities import Household, Person from openfisca_core import holders, periods, simulations -from openfisca_core.parameters import ValuesHistory, ParameterNode -from openfisca_core.periods import DateUnit -from openfisca_core.periods import Instant +from openfisca_core.parameters import ParameterNode, ValuesHistory +from openfisca_core.periods import DateUnit, Instant from openfisca_core.reforms import Reform from openfisca_core.tools import assert_near from openfisca_core.variables import Variable diff --git a/tests/core/test_simulation_builder.py b/tests/core/test_simulation_builder.py index 464401d99a..d1dc0cde75 100644 --- a/tests/core/test_simulation_builder.py +++ b/tests/core/test_simulation_builder.py @@ -1,6 +1,7 @@ -import datetime from typing import Iterable +import datetime + import pytest from openfisca_country_template import entities, situation_examples diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index b0fa80598f..1ecfd09aa6 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -1,19 +1,22 @@ # -*- coding: utf-8 -*- +import csv import json import os -import csv + import numpy -from pytest import fixture, mark, raises, approx +from pytest import approx, fixture, mark, raises -from openfisca_core.simulations import Simulation, CycleError, SpiralError +from openfisca_country_template.variables.housing import HousingOccupancyStatus + +from openfisca_core.simulations import CycleError, Simulation, SpiralError from openfisca_core.tracers import ( - SimpleTracer, FullTracer, - TracingParameterNodeAtInstant, + SimpleTracer, TraceNode, + TracingParameterNodeAtInstant, ) -from openfisca_country_template.variables.housing import HousingOccupancyStatus + from .parameters_fancy_indexing.test_fancy_indexing import parameters diff --git a/tests/core/test_yaml.py b/tests/core/test_yaml.py index 6ca8bb9148..4673665fcb 100644 --- a/tests/core/test_yaml.py +++ b/tests/core/test_yaml.py @@ -2,10 +2,10 @@ import subprocess import pytest + import openfisca_extension_template from openfisca_core.tools.test_runner import run_tests - from tests.fixtures import yaml_tests yaml_tests_dir = os.path.dirname(yaml_tests.__file__) diff --git a/tests/core/tools/test_runner/test_yaml_runner.py b/tests/core/tools/test_runner/test_yaml_runner.py index 82ff4fe5e7..bf04ade9bb 100644 --- a/tests/core/tools/test_runner/test_yaml_runner.py +++ b/tests/core/tools/test_runner/test_yaml_runner.py @@ -1,15 +1,16 @@ -import os from typing import List -import pytest +import os + import numpy +import pytest -from openfisca_core.tools.test_runner import _get_tax_benefit_system, YamlItem, YamlFile -from openfisca_core.errors import VariableNotFound -from openfisca_core.variables import Variable -from openfisca_core.populations import Population +from openfisca_core import errors from openfisca_core.entities import Entity from openfisca_core.periods import DateUnit +from openfisca_core.populations import Population +from openfisca_core.tools.test_runner import YamlFile, YamlItem, _get_tax_benefit_system +from openfisca_core.variables import Variable class TaxBenefitSystem: @@ -86,7 +87,7 @@ def __init__(self): @pytest.mark.skip(reason="Deprecated node constructor") def test_variable_not_found(): test = {"output": {"unknown_variable": 0}} - with pytest.raises(VariableNotFound) as excinfo: + with pytest.raises(errors.VariableNotFoundError) as excinfo: test_item = TestItem(test) test_item.check_output() assert excinfo.value.variable_name == "unknown_variable" diff --git a/tests/core/variables/test_variables.py b/tests/core/variables/test_variables.py index 15c482b73b..3b2790bae7 100644 --- a/tests/core/variables/test_variables.py +++ b/tests/core/variables/test_variables.py @@ -2,7 +2,7 @@ import datetime -from pytest import fixture, raises, mark +from pytest import fixture, mark, raises import openfisca_country_template as country_template import openfisca_country_template.situation_examples @@ -13,7 +13,6 @@ from openfisca_core.tools import assert_near from openfisca_core.variables import Variable - # Check which date is applied whether it comes from Variable attribute (end) # or formula(s) dates. diff --git a/tests/web_api/basic_case/__init__.py b/tests/web_api/basic_case/__init__.py index fe069a32e5..bb39b2df14 100644 --- a/tests/web_api/basic_case/__init__.py +++ b/tests/web_api/basic_case/__init__.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import pkg_resources -from openfisca_web_api.app import create_app + from openfisca_core.scripts import build_tax_benefit_system +from openfisca_web_api.app import create_app TEST_COUNTRY_PACKAGE_NAME = "openfisca_country_template" distribution = pkg_resources.get_distribution(TEST_COUNTRY_PACKAGE_NAME) diff --git a/tests/web_api/case_with_extension/test_extensions.py b/tests/web_api/case_with_extension/test_extensions.py index 3bb3c956b4..7984025196 100644 --- a/tests/web_api/case_with_extension/test_extensions.py +++ b/tests/web_api/case_with_extension/test_extensions.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from http.client import OK + from openfisca_core.scripts import build_tax_benefit_system from openfisca_web_api.app import create_app - TEST_COUNTRY_PACKAGE_NAME = "openfisca_country_template" TEST_EXTENSION_PACKAGE_NAMES = ["openfisca_extension_template"] diff --git a/tests/web_api/case_with_reform/test_reforms.py b/tests/web_api/case_with_reform/test_reforms.py index 5c3a241fe9..993dceda45 100644 --- a/tests/web_api/case_with_reform/test_reforms.py +++ b/tests/web_api/case_with_reform/test_reforms.py @@ -1,4 +1,5 @@ import http + import pytest from openfisca_core import scripts diff --git a/tests/web_api/loader/test_parameters.py b/tests/web_api/loader/test_parameters.py index c66befea3f..2b6be58916 100644 --- a/tests/web_api/loader/test_parameters.py +++ b/tests/web_api/loader/test_parameters.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- from openfisca_core.parameters import Scale - -from openfisca_web_api.loader.parameters import build_api_scale, build_api_parameter +from openfisca_web_api.loader.parameters import build_api_parameter, build_api_scale def test_build_rate_scale(): diff --git a/tests/web_api/test_calculate.py b/tests/web_api/test_calculate.py index baab3575ef..d5d64c3c38 100644 --- a/tests/web_api/test_calculate.py +++ b/tests/web_api/test_calculate.py @@ -1,8 +1,9 @@ import copy -import dpath.util import json -from http import client import os +from http import client + +import dpath.util import pytest from openfisca_country_template.situation_examples import couple diff --git a/tests/web_api/test_entities.py b/tests/web_api/test_entities.py index 26e28a9ddd..afb909ef57 100644 --- a/tests/web_api/test_entities.py +++ b/tests/web_api/test_entities.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -from http import client import json +from http import client from openfisca_country_template import entities - # /entities diff --git a/tests/web_api/test_helpers.py b/tests/web_api/test_helpers.py index 94c650e5c9..5b22a57b47 100644 --- a/tests/web_api/test_helpers.py +++ b/tests/web_api/test_helpers.py @@ -1,9 +1,7 @@ import os -from openfisca_web_api.loader import parameters - from openfisca_core.parameters import load_parameter_file - +from openfisca_web_api.loader import parameters dir_path = os.path.join(os.path.dirname(__file__), "assets") diff --git a/tests/web_api/test_parameters.py b/tests/web_api/test_parameters.py index 9ee091fccb..762193fc2d 100644 --- a/tests/web_api/test_parameters.py +++ b/tests/web_api/test_parameters.py @@ -1,8 +1,8 @@ -from http import client import json -import pytest import re +from http import client +import pytest # /parameters diff --git a/tests/web_api/test_spec.py b/tests/web_api/test_spec.py index 605eb1815e..89f221ee0e 100644 --- a/tests/web_api/test_spec.py +++ b/tests/web_api/test_spec.py @@ -1,9 +1,9 @@ -import dpath.util import json from http import client -from openapi_spec_validator import openapi_v3_spec_validator +import dpath.util import pytest +from openapi_spec_validator import openapi_v3_spec_validator def assert_items_equal(x, y): diff --git a/tests/web_api/test_trace.py b/tests/web_api/test_trace.py index eab41d3130..ee6c6ab21f 100644 --- a/tests/web_api/test_trace.py +++ b/tests/web_api/test_trace.py @@ -1,9 +1,10 @@ import copy -import dpath.util -from http import client import json +from http import client + +import dpath.util -from openfisca_country_template.situation_examples import single, couple +from openfisca_country_template.situation_examples import couple, single def assert_items_equal(x, y): diff --git a/tests/web_api/test_variables.py b/tests/web_api/test_variables.py index d343f8d2ae..d53581618d 100644 --- a/tests/web_api/test_variables.py +++ b/tests/web_api/test_variables.py @@ -1,7 +1,8 @@ -from http import client import json -import pytest import re +from http import client + +import pytest def assert_items_equal(x, y): From a0bd24bc2471eda6add3115b7b5bb825d7d28fe0 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 26 Jun 2023 13:48:48 +0200 Subject: [PATCH 019/188] Bump version --- CHANGELOG.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d3d72ebc8..7493c4e258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +### 40.0.2 [#1186](https://github.com/openfisca/openfisca-core/pull/1186) + +#### Technical changes + +- Skip type-checking tasks + - Before their definition was commented out but still run with `make test` + - Now they're skipped but not commented, which is needed to fix the + underlying issues + ## 41.1.0 [#1195](https://github.com/openfisca/openfisca-core/pull/1195) #### Technical changes diff --git a/setup.py b/setup.py index 3d77867b1f..99a3c00e92 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.1.0", + version="41.1.1", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From c8331465c081b3cecf34e1e47b159cb9738b2956 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Sep 2023 23:21:16 +0200 Subject: [PATCH 020/188] Add test to role --- openfisca_core/entities/tests/__init__.py | 0 openfisca_core/entities/tests/test_role.py | 11 +++++++++++ 2 files changed, 11 insertions(+) create mode 100644 openfisca_core/entities/tests/__init__.py create mode 100644 openfisca_core/entities/tests/test_role.py diff --git a/openfisca_core/entities/tests/__init__.py b/openfisca_core/entities/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py new file mode 100644 index 0000000000..99b047554c --- /dev/null +++ b/openfisca_core/entities/tests/test_role.py @@ -0,0 +1,11 @@ +from openfisca_core import entities + + +def test_init_when_doc_indented(): + """De-indents the ``doc`` attribute if it is passed at initialisation.""" + + key = "\tkey" + doc = "\tdoc" + role = entities.Role({"key": key, "doc": doc}, object()) + assert role.key == key + assert role.doc != doc From 915000d5220df951f31ab7f9fb832e1b309b8126 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Sep 2023 23:23:07 +0200 Subject: [PATCH 021/188] Add test to entity --- openfisca_core/entities/tests/test_entity.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 openfisca_core/entities/tests/test_entity.py diff --git a/openfisca_core/entities/tests/test_entity.py b/openfisca_core/entities/tests/test_entity.py new file mode 100644 index 0000000000..7d6476eaa7 --- /dev/null +++ b/openfisca_core/entities/tests/test_entity.py @@ -0,0 +1,11 @@ +from openfisca_core import entities + + +def test_init_when_doc_indented(): + """De-indents the ``doc`` attribute if it is passed at initialisation.""" + + key = "\tkey" + doc = "\tdoc" + entity = entities.Entity(key, "label", "plural", doc) + assert entity.key == key + assert entity.doc != doc From bf1bbfc064e24acee870e2332eb5015f724c7492 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 00:38:31 +0200 Subject: [PATCH 022/188] Add test to group entity --- openfisca_core/entities/__init__.py | 10 ++- openfisca_core/entities/entity.py | 2 +- openfisca_core/entities/group_entity.py | 12 ++- openfisca_core/entities/helpers.py | 7 +- openfisca_core/entities/tests/test_entity.py | 4 +- .../entities/tests/test_group_entity.py | 84 +++++++++++++++++++ openfisca_core/entities/tests/test_role.py | 4 +- openfisca_core/entities/typing.py | 8 ++ 8 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 openfisca_core/entities/tests/test_group_entity.py diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index a081987049..73811a2748 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -21,7 +21,9 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .entity import Entity # noqa: F401 -from .group_entity import GroupEntity # noqa: F401 -from .helpers import build_entity # noqa: F401 -from .role import Role # noqa: F401 +from .entity import Entity +from .group_entity import GroupEntity +from .helpers import build_entity +from .role import Role + +__all__ = ["Entity", "GroupEntity", "Role", "build_entity"] diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index b0f9e2a0d4..53736c4601 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -12,7 +12,7 @@ class Entity: Represents an entity (e.g. a person, a household, etc.) on which calculations can be run. """ - def __init__(self, key, plural, label, doc): + def __init__(self, key: str, plural: str, label: str, doc: str) -> None: self.key = key self.label = label self.plural = plural diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index f6472d2c52..802e69fab7 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,3 +1,5 @@ +from typing import Sequence + from .entity import Entity from .role import Role @@ -24,7 +26,15 @@ class GroupEntity(Entity): """ # noqa RST301 - def __init__(self, key, plural, label, doc, roles, containing_entities=()): + def __init__( + self, + key: str, + plural: str, + label: str, + doc: str, + roles: Sequence[Role], + containing_entities: Sequence[str] = (), + ) -> object: super().__init__(key, plural, label, doc) self.roles_description = roles self.roles = [] diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 6e0ab55aca..43969fcd7a 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,4 +1,5 @@ -from openfisca_core import entities +from .entity import Entity +from .group_entity import GroupEntity def build_entity( @@ -12,8 +13,8 @@ def build_entity( containing_entities=(), ): if is_person: - return entities.Entity(key, plural, label, doc) + return Entity(key, plural, label, doc) else: - return entities.GroupEntity( + return GroupEntity( key, plural, label, doc, roles, containing_entities=containing_entities ) diff --git a/openfisca_core/entities/tests/test_entity.py b/openfisca_core/entities/tests/test_entity.py index 7d6476eaa7..3bf09b415e 100644 --- a/openfisca_core/entities/tests/test_entity.py +++ b/openfisca_core/entities/tests/test_entity.py @@ -1,8 +1,8 @@ from openfisca_core import entities -def test_init_when_doc_indented(): - """De-indents the ``doc`` attribute if it is passed at initialisation.""" +def test_init_when_doc_indented() -> None: + """De-indent the ``doc`` attribute if it is passed at initialisation.""" key = "\tkey" doc = "\tdoc" diff --git a/openfisca_core/entities/tests/test_group_entity.py b/openfisca_core/entities/tests/test_group_entity.py new file mode 100644 index 0000000000..0078ac5818 --- /dev/null +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -0,0 +1,84 @@ +import typing + +import pytest + +from openfisca_core import entities + + +@pytest.fixture +def parent() -> str: + """A key.""" + + return "parent" + + +@pytest.fixture +def uncle() -> str: + """Another key.""" + + return "uncle" + + +@pytest.fixture +def first_parent() -> str: + """A sub-role.""" + + return "first_parent" + + +@pytest.fixture +def second_parent() -> str: + """Another sub-role.""" + + return "second_parent" + + +@pytest.fixture +def third_parent() -> str: + """Yet another sub-role.""" + + return "third_parent" + + +@pytest.fixture +def role(parent: str, first_parent: str, third_parent: str) -> entities.Role: + """A role.""" + + return typing.cast( + entities.Role, {"key": parent, "subroles": [first_parent, third_parent]} + ) + + +@pytest.fixture +def group_entity(role: entities.Role) -> entities.GroupEntity: + """A group entity.""" + + return entities.GroupEntity("key", "label", "plural", "doc", [role]) + + +def test_init_when_doc_indented() -> None: + """De-indent the ``doc`` attribute if it is passed at initialisation.""" + + key = "\tkey" + doc = "\tdoc" + group_entity = entities.GroupEntity(key, "label", "plural", doc, []) + assert group_entity.key == key + assert group_entity.doc != doc + + +def test_group_entity_with_roles( + group_entity: entities.GroupEntity, parent: str, uncle: str +) -> None: + """Assign a role for each role-like passed as argument.""" + + assert hasattr(group_entity, parent.upper()) + assert not hasattr(group_entity, uncle.upper()) + + +def test_group_entity_with_subroles( + group_entity: entities.GroupEntity, first_parent: str, second_parent: str +) -> None: + """Assign a role for each sub-role-like passed as argument.""" + + assert hasattr(group_entity, first_parent.upper()) + assert not hasattr(group_entity, second_parent.upper()) diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py index 99b047554c..6a2e8f39b8 100644 --- a/openfisca_core/entities/tests/test_role.py +++ b/openfisca_core/entities/tests/test_role.py @@ -1,8 +1,8 @@ from openfisca_core import entities -def test_init_when_doc_indented(): - """De-indents the ``doc`` attribute if it is passed at initialisation.""" +def test_init_when_doc_indented() -> None: + """De-indent the ``doc`` attribute if it is passed at initialisation.""" key = "\tkey" doc = "\tdoc" diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py index ab2640d21a..a4c9af93a0 100644 --- a/openfisca_core/entities/typing.py +++ b/openfisca_core/entities/typing.py @@ -7,3 +7,11 @@ class Entity(Protocol): .. versionadded:: 41.0.1 """ + + +class Role(Protocol): + """Role protocol. + + .. versionadded:: 41.1.0 + + """ From 3b73a6fe6f41964aef1487719fd08fc4aa23ebb6 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 02:25:16 +0200 Subject: [PATCH 023/188] Fix syntax & style --- openfisca_core/entities/entity.py | 6 +++--- openfisca_core/entities/group_entity.py | 5 +++-- openfisca_core/entities/role.py | 6 +++--- .../entities/tests/test_group_entity.py | 12 ++++++------ openfisca_core/entities/tests/test_role.py | 17 +++++++++++++++-- openfisca_core/entities/typing.py | 19 ++++++++++--------- openfisca_tasks/lint.mk | 18 +----------------- 7 files changed, 41 insertions(+), 42 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 53736c4601..bb410a874e 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -25,7 +25,7 @@ def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystem): def check_role_validity(self, role: Any) -> None: if role is not None and not isinstance(role, Role): - raise ValueError("{} is not a valid role".format(role)) + raise ValueError(f"{role} is not a valid role") def get_variable( self, @@ -46,10 +46,10 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None: if entity.key != self.key: message = os.linesep.join( [ - "You tried to compute the variable '{0}' for the entity '{1}';".format( + "You tried to compute the variable '{}' for the entity '{}';".format( variable_name, self.plural ), - "however the variable '{0}' is defined for '{1}'.".format( + "however the variable '{}' is defined for '{}'.".format( variable_name, entity.plural ), "Learn more about entities in our documentation:", diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index 802e69fab7..5c224096f7 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,7 +1,8 @@ -from typing import Sequence +from collections.abc import Sequence from .entity import Entity from .role import Role +from .typing import HasKey class GroupEntity(Entity): @@ -32,7 +33,7 @@ def __init__( plural: str, label: str, doc: str, - roles: Sequence[Role], + roles: Sequence[HasKey], containing_entities: Sequence[str] = (), ) -> object: super().__init__(key, plural, label, doc) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 5577da3655..11da1e2c51 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -5,7 +5,7 @@ import dataclasses import textwrap -from .typing import Entity +from .typing import HasKey class Role: @@ -49,9 +49,9 @@ class Role: """ - def __init__(self, description: dict[str, Any], entity: Entity) -> None: + def __init__(self, description: dict[str, Any], entity: HasKey) -> None: role_description: _RoleDescription = _RoleDescription(**description) - self.entity: Entity = entity + self.entity: HasKey = entity self.key: str = role_description.key self.plural: str | None = role_description.plural self.label: str | None = role_description.label diff --git a/openfisca_core/entities/tests/test_group_entity.py b/openfisca_core/entities/tests/test_group_entity.py index 0078ac5818..e379a90d98 100644 --- a/openfisca_core/entities/tests/test_group_entity.py +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -1,9 +1,11 @@ -import typing +from typing import Any import pytest from openfisca_core import entities +from ..typing import HasKey + @pytest.fixture def parent() -> str: @@ -41,16 +43,14 @@ def third_parent() -> str: @pytest.fixture -def role(parent: str, first_parent: str, third_parent: str) -> entities.Role: +def role(parent: str, first_parent: str, third_parent: str) -> Any: """A role.""" - return typing.cast( - entities.Role, {"key": parent, "subroles": [first_parent, third_parent]} - ) + return {"key": parent, "subroles": [first_parent, third_parent]} @pytest.fixture -def group_entity(role: entities.Role) -> entities.GroupEntity: +def group_entity(role: HasKey) -> entities.GroupEntity: """A group entity.""" return entities.GroupEntity("key", "label", "plural", "doc", [role]) diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py index 6a2e8f39b8..13451af8aa 100644 --- a/openfisca_core/entities/tests/test_role.py +++ b/openfisca_core/entities/tests/test_role.py @@ -1,11 +1,24 @@ +from typing import Any + +import pytest + from openfisca_core import entities +from ..typing import HasKey + + +@pytest.fixture +def entity() -> Any: + """An entity.""" + + return object() + -def test_init_when_doc_indented() -> None: +def test_init_when_doc_indented(entity: HasKey) -> None: """De-indent the ``doc`` attribute if it is passed at initialisation.""" key = "\tkey" doc = "\tdoc" - role = entities.Role({"key": key, "doc": doc}, object()) + role = entities.Role({"key": key, "doc": doc}, entity) assert role.key == key assert role.doc != doc diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py index a4c9af93a0..f4d3a68109 100644 --- a/openfisca_core/entities/typing.py +++ b/openfisca_core/entities/typing.py @@ -1,17 +1,18 @@ +from abc import abstractmethod from typing import Protocol -class Entity(Protocol): - """Entity protocol. +class HasKey(Protocol): + """Protocol of objects having a key. .. versionadded:: 41.0.1 - """ - - -class Role(Protocol): - """Role protocol. - - .. versionadded:: 41.1.0 + .. versionchanged:: 41.0.2 + Renamed from ``Entity`` to make it generic. """ + + @property + @abstractmethod + def key(self) -> str: + """A key to identify something.""" diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 69a1a50957..7b958dd1e9 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -36,23 +36,7 @@ lint-doc-%: ## Run static type checkers for type errors. check-types: @$(call print_help,$@:) - @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.$* + @mypy openfisca_core/entities @$(call print_pass,$@:) ## Run code formatters to correct style errors. From e18d11e44d7d528f2748d1575cc2f1b241dbc9be Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 07:33:23 +0200 Subject: [PATCH 024/188] Remove version changed from inline doc --- openfisca_core/entities/typing.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py index f4d3a68109..2a88eee173 100644 --- a/openfisca_core/entities/typing.py +++ b/openfisca_core/entities/typing.py @@ -7,9 +7,6 @@ class HasKey(Protocol): .. versionadded:: 41.0.1 - .. versionchanged:: 41.0.2 - Renamed from ``Entity`` to make it generic. - """ @property From 3bd0b28fdf9247dc3fa4ce9c1356591c72258fb3 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 07:35:45 +0200 Subject: [PATCH 025/188] Fix group entity signature --- openfisca_core/entities/group_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index 5c224096f7..ec1858f53c 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -35,7 +35,7 @@ def __init__( doc: str, roles: Sequence[HasKey], containing_entities: Sequence[str] = (), - ) -> object: + ) -> None: super().__init__(key, plural, label, doc) self.roles_description = roles self.roles = [] From de08271e65357a13fc5c2a964ad103c59b82d6a2 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 08:54:04 +0200 Subject: [PATCH 026/188] Do not force ordered roles --- openfisca_core/entities/group_entity.py | 19 +++++++++---------- openfisca_core/entities/role.py | 7 ++++--- .../entities/tests/test_group_entity.py | 11 +++++------ 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index ec1858f53c..0c787da5b9 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,8 +1,10 @@ -from collections.abc import Sequence +from collections.abc import Iterable, Mapping +from typing import Any + +from itertools import chain from .entity import Entity from .role import Role -from .typing import HasKey class GroupEntity(Entity): @@ -21,10 +23,6 @@ class GroupEntity(Entity): containing_entities: The list of keys of group entities whose members are guaranteed to be a superset of this group's entities. - .. versionchanged:: 35.7.0 - Added ``containing_entities``, that allows the defining of group - entities which entirely contain other group entities. - """ # noqa RST301 def __init__( @@ -33,8 +31,8 @@ def __init__( plural: str, label: str, doc: str, - roles: Sequence[HasKey], - containing_entities: Sequence[str] = (), + roles: Iterable[Mapping[str, Any]], + containing_entities: Iterable[str] = (), ) -> None: super().__init__(key, plural, label, doc) self.roles_description = roles @@ -50,8 +48,9 @@ def __init__( setattr(self, subrole.key.upper(), subrole) role.subroles.append(subrole) role.max = len(role.subroles) - self.flattened_roles = sum( - [role2.subroles or [role2] for role2 in self.roles], [] + self.flattened_roles = tuple( + chain.from_iterable(role.subroles or [role] for role in self.roles) ) + self.is_person = False self.containing_entities = containing_entities diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 11da1e2c51..5ca4d76f3a 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Iterable, Mapping from typing import Any import dataclasses @@ -49,7 +50,7 @@ class Role: """ - def __init__(self, description: dict[str, Any], entity: HasKey) -> None: + def __init__(self, description: Mapping[str, Any], entity: HasKey) -> None: role_description: _RoleDescription = _RoleDescription(**description) self.entity: HasKey = entity self.key: str = role_description.key @@ -57,7 +58,7 @@ def __init__(self, description: dict[str, Any], entity: HasKey) -> None: self.label: str | None = role_description.label self.doc: str = role_description.doc self.max: int | None = role_description.max - self.subroles: list[Role] | None = None + self.subroles: Iterable[Role] | None = None def __repr__(self) -> str: return f"Role({self.key})" @@ -113,7 +114,7 @@ class _RoleDescription: max: int | None = None #: A list of subroles. - subroles: list[str] | None = None + subroles: Iterable[str] | None = None def __post_init__(self) -> None: object.__setattr__(self, "doc", textwrap.dedent(self.doc)) diff --git a/openfisca_core/entities/tests/test_group_entity.py b/openfisca_core/entities/tests/test_group_entity.py index e379a90d98..26cc0940c2 100644 --- a/openfisca_core/entities/tests/test_group_entity.py +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -1,11 +1,10 @@ +from collections.abc import Mapping from typing import Any import pytest from openfisca_core import entities -from ..typing import HasKey - @pytest.fixture def parent() -> str: @@ -46,14 +45,14 @@ def third_parent() -> str: def role(parent: str, first_parent: str, third_parent: str) -> Any: """A role.""" - return {"key": parent, "subroles": [first_parent, third_parent]} + return {"key": parent, "subroles": {first_parent, third_parent}} @pytest.fixture -def group_entity(role: HasKey) -> entities.GroupEntity: +def group_entity(role: Mapping[str, Any]) -> entities.GroupEntity: """A group entity.""" - return entities.GroupEntity("key", "label", "plural", "doc", [role]) + return entities.GroupEntity("key", "label", "plural", "doc", (role,)) def test_init_when_doc_indented() -> None: @@ -61,7 +60,7 @@ def test_init_when_doc_indented() -> None: key = "\tkey" doc = "\tdoc" - group_entity = entities.GroupEntity(key, "label", "plural", doc, []) + group_entity = entities.GroupEntity(key, "label", "plural", doc, ()) assert group_entity.key == key assert group_entity.doc != doc From 31c40da9625a972406925492329b06c9e21ed74f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 16:17:49 +0200 Subject: [PATCH 027/188] Reuse Description for composition --- openfisca_core/entities/__init__.py | 4 +- openfisca_core/entities/role.py | 98 ++++++++++++++-------- openfisca_core/entities/tests/test_role.py | 4 +- openfisca_core/entities/typing.py | 14 +--- 4 files changed, 68 insertions(+), 52 deletions(-) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index 73811a2748..a7467f83cb 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -21,9 +21,11 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports +import typing + from .entity import Entity from .group_entity import GroupEntity from .helpers import build_entity from .role import Role -__all__ = ["Entity", "GroupEntity", "Role", "build_entity"] +__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "typing"] diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 5ca4d76f3a..4e73773738 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,12 +1,12 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping +from collections.abc import Sequence, Mapping from typing import Any import dataclasses import textwrap -from .typing import HasKey +from .typing import Entity class Role: @@ -18,11 +18,8 @@ class Role: several dependents, and the like. Attributes: - entity (Entity): The Entity to which the Role belongs. - key (str): A key to identify the Role. - plural (str): The ``key``, pluralised. - label (str): A summary description. - doc (str): A full description, dedented. + entity (Entity): The Entity the Role belongs to. + description (_Description): A description of the Role. max (int): Max number of members. subroles (list[Role]): A list of subroles. @@ -50,48 +47,80 @@ class Role: """ - def __init__(self, description: Mapping[str, Any], entity: HasKey) -> None: - role_description: _RoleDescription = _RoleDescription(**description) - self.entity: HasKey = entity - self.key: str = role_description.key - self.plural: str | None = role_description.plural - self.label: str | None = role_description.label - self.doc: str = role_description.doc - self.max: int | None = role_description.max - self.subroles: Iterable[Role] | None = None + #: The Entity the Role belongs to. + entity: Entity + + #: A description of the Role. + description: _Description + + #: Max number of members. + max: int | None = None + + #: A list of subroles. + subroles: Sequence[Role] | None = None + + @property + def key(self) -> str: + """A key to identify the Role.""" + return self.description.key + + @property + def plural(self) -> str | None: + """The ``key``, pluralised.""" + return self.description.plural + + @property + def label(self) -> str | None: + """A summary description.""" + return self.description.label + + @property + def doc(self) -> str | None: + """A full description, non-indented.""" + return self.description.doc + + def __init__(self, description: Mapping[str, Any], entity: Entity) -> None: + self.description = _Description( + **{ + key: value + for key, value in description.items() + if key in {"key", "plural", "label", "doc"} + } + ) + self.entity = entity + self.max = description.get("max") def __repr__(self) -> str: return f"Role({self.key})" @dataclasses.dataclass(frozen=True) -class _RoleDescription: +class _Description: """A Role's description. Examples: - >>> description = { + >>> data = { ... "key": "parent", ... "label": "Parents", ... "plural": "parents", ... "doc": "\t\t\tThe one/two adults in charge of the household.", - ... "max": 2, ... } - >>> role_description = _RoleDescription(**description) + >>> description = _Description(**data) - >>> repr(_RoleDescription) - "" + >>> repr(_Description) + "" - >>> repr(role_description) - "_RoleDescription(key='parent', plural='parents', label='Parents',...)" + >>> repr(description) + "_Description(key='parent', plural='parents', label='Parents', ...)" - >>> str(role_description) - "_RoleDescription(key='parent', plural='parents', label='Parents',...)" + >>> str(description) + "_Description(key='parent', plural='parents', label='Parents', ...)" - >>> {role_description} - {...} + >>> {description} + {_Description(key='parent', plural='parents', label='Parents', doc=...} - >>> role_description.key + >>> description.key 'parent' .. versionadded:: 41.0.1 @@ -108,13 +137,8 @@ class _RoleDescription: label: str | None = None #: A full description, non-indented. - doc: str = "" - - #: Max number of members. - max: int | None = None - - #: A list of subroles. - subroles: Iterable[str] | None = None + doc: str | None = None def __post_init__(self) -> None: - object.__setattr__(self, "doc", textwrap.dedent(self.doc)) + if self.doc is not None: + object.__setattr__(self, "doc", textwrap.dedent(self.doc)) diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py index 13451af8aa..a784c843ba 100644 --- a/openfisca_core/entities/tests/test_role.py +++ b/openfisca_core/entities/tests/test_role.py @@ -4,7 +4,7 @@ from openfisca_core import entities -from ..typing import HasKey +from openfisca_core.entities.typing import Entity @pytest.fixture @@ -14,7 +14,7 @@ def entity() -> Any: return object() -def test_init_when_doc_indented(entity: HasKey) -> None: +def test_init_when_doc_indented(entity: Entity) -> None: """De-indent the ``doc`` attribute if it is passed at initialisation.""" key = "\tkey" diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py index 2a88eee173..5a1e60d4ad 100644 --- a/openfisca_core/entities/typing.py +++ b/openfisca_core/entities/typing.py @@ -1,15 +1,5 @@ -from abc import abstractmethod from typing import Protocol -class HasKey(Protocol): - """Protocol of objects having a key. - - .. versionadded:: 41.0.1 - - """ - - @property - @abstractmethod - def key(self) -> str: - """A key to identify something.""" +class Entity(Protocol): + ... From 6371b00c99df6afe928336957c4af3031009a1a0 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 16:20:20 +0200 Subject: [PATCH 028/188] Improve de-indent test --- openfisca_core/entities/tests/test_entity.py | 2 +- openfisca_core/entities/tests/test_group_entity.py | 2 +- openfisca_core/entities/tests/test_role.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openfisca_core/entities/tests/test_entity.py b/openfisca_core/entities/tests/test_entity.py index 3bf09b415e..488d271ff5 100644 --- a/openfisca_core/entities/tests/test_entity.py +++ b/openfisca_core/entities/tests/test_entity.py @@ -8,4 +8,4 @@ def test_init_when_doc_indented() -> None: doc = "\tdoc" entity = entities.Entity(key, "label", "plural", doc) assert entity.key == key - assert entity.doc != doc + assert entity.doc == doc.lstrip() diff --git a/openfisca_core/entities/tests/test_group_entity.py b/openfisca_core/entities/tests/test_group_entity.py index 26cc0940c2..882e777391 100644 --- a/openfisca_core/entities/tests/test_group_entity.py +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -62,7 +62,7 @@ def test_init_when_doc_indented() -> None: doc = "\tdoc" group_entity = entities.GroupEntity(key, "label", "plural", doc, ()) assert group_entity.key == key - assert group_entity.doc != doc + assert group_entity.doc == doc.lstrip() def test_group_entity_with_roles( diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py index a784c843ba..13fda94654 100644 --- a/openfisca_core/entities/tests/test_role.py +++ b/openfisca_core/entities/tests/test_role.py @@ -21,4 +21,4 @@ def test_init_when_doc_indented(entity: Entity) -> None: doc = "\tdoc" role = entities.Role({"key": key, "doc": doc}, entity) assert role.key == key - assert role.doc != doc + assert role.doc == doc.lstrip() From d7a4a4423a6d3c261a50d1d7f05083d4317e3585 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 16:27:16 +0200 Subject: [PATCH 029/188] Clarify type hints --- openfisca_core/entities/role.py | 2 +- .../entities/tests/test_group_entity.py | 20 +++---------------- openfisca_core/entities/tests/test_role.py | 17 ++-------------- 3 files changed, 6 insertions(+), 33 deletions(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 4e73773738..976711b609 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Sequence, Mapping +from collections.abc import Mapping, Sequence from typing import Any import dataclasses diff --git a/openfisca_core/entities/tests/test_group_entity.py b/openfisca_core/entities/tests/test_group_entity.py index 882e777391..ed55648d71 100644 --- a/openfisca_core/entities/tests/test_group_entity.py +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -8,50 +8,36 @@ @pytest.fixture def parent() -> str: - """A key.""" - return "parent" @pytest.fixture def uncle() -> str: - """Another key.""" - return "uncle" @pytest.fixture def first_parent() -> str: - """A sub-role.""" - return "first_parent" @pytest.fixture def second_parent() -> str: - """Another sub-role.""" - return "second_parent" @pytest.fixture def third_parent() -> str: - """Yet another sub-role.""" - return "third_parent" @pytest.fixture -def role(parent: str, first_parent: str, third_parent: str) -> Any: - """A role.""" - +def role(parent: str, first_parent: str, third_parent: str) -> Mapping[str, Any]: return {"key": parent, "subroles": {first_parent, third_parent}} @pytest.fixture def group_entity(role: Mapping[str, Any]) -> entities.GroupEntity: - """A group entity.""" - return entities.GroupEntity("key", "label", "plural", "doc", (role,)) @@ -68,7 +54,7 @@ def test_init_when_doc_indented() -> None: def test_group_entity_with_roles( group_entity: entities.GroupEntity, parent: str, uncle: str ) -> None: - """Assign a role for each role-like passed as argument.""" + """Assign a Role for each role-like passed as argument.""" assert hasattr(group_entity, parent.upper()) assert not hasattr(group_entity, uncle.upper()) @@ -77,7 +63,7 @@ def test_group_entity_with_roles( def test_group_entity_with_subroles( group_entity: entities.GroupEntity, first_parent: str, second_parent: str ) -> None: - """Assign a role for each sub-role-like passed as argument.""" + """Assign a Role for each subrole-like passed as argument.""" assert hasattr(group_entity, first_parent.upper()) assert not hasattr(group_entity, second_parent.upper()) diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py index 13fda94654..83692e8236 100644 --- a/openfisca_core/entities/tests/test_role.py +++ b/openfisca_core/entities/tests/test_role.py @@ -1,24 +1,11 @@ -from typing import Any - -import pytest - from openfisca_core import entities -from openfisca_core.entities.typing import Entity - - -@pytest.fixture -def entity() -> Any: - """An entity.""" - - return object() - -def test_init_when_doc_indented(entity: Entity) -> None: +def test_init_when_doc_indented() -> None: """De-indent the ``doc`` attribute if it is passed at initialisation.""" key = "\tkey" doc = "\tdoc" - role = entities.Role({"key": key, "doc": doc}, entity) + role = entities.Role({"key": key, "doc": doc}, object()) assert role.key == key assert role.doc == doc.lstrip() From f652bfae34dac64a03a2c7d4159ca70eb0b11c2b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 16:30:30 +0200 Subject: [PATCH 030/188] Fix bad import --- openfisca_core/entities/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index a7467f83cb..c90b9d0d6b 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -21,8 +21,7 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -import typing - +from . import typing from .entity import Entity from .group_entity import GroupEntity from .helpers import build_entity From f79e3e14ad4ec6b04f5ee083c9f826d7172b56a5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 4 Oct 2023 01:15:39 +0200 Subject: [PATCH 031/188] Use f-string in error message --- openfisca_core/entities/entity.py | 19 +++++++------------ pyproject.toml | 2 ++ 2 files changed, 9 insertions(+), 12 deletions(-) create mode 100644 pyproject.toml diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index bb410a874e..2501cec80c 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -44,16 +44,11 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None: entity = variable.entity if entity.key != self.key: - message = os.linesep.join( - [ - "You tried to compute the variable '{}' for the entity '{}';".format( - variable_name, self.plural - ), - "however the variable '{}' is defined for '{}'.".format( - variable_name, entity.plural - ), - "Learn more about entities in our documentation:", - ".", - ] + message = ( + f"You tried to compute the variable '{variable_name}' for", + f"the entity '{self.plural}'; however the variable", + f"'{variable_name}' is defined for '{entity.plural}'.", + "Learn more about entities in our documentation:", + ".", ) - raise ValueError(message) + raise ValueError(os.linesep.join(message)) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000..1e2a43ee4e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +target-version = ["py39", "py310", "py311"] From 696e00f29fc211264c2d0a084468eb3654d8b82d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 27 Sep 2023 02:28:17 +0200 Subject: [PATCH 032/188] Bump version --- CHANGELOG.md | 8 +++++++- setup.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7493c4e258..9f3d7d2789 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog -### 40.0.2 [#1186](https://github.com/openfisca/openfisca-core/pull/1186) +### 41.1.2 [#1192](https://github.com/openfisca/openfisca-core/pull/1192) + +#### Technical changes + +- Add tests to `entities`. + +### 41.1.1 [#1186](https://github.com/openfisca/openfisca-core/pull/1186) #### Technical changes diff --git a/setup.py b/setup.py index 99a3c00e92..07fd5c2c9c 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.1.1", + version="41.1.2", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 51cfe853d21bdebcee9671bee0ee9747d4540193 Mon Sep 17 00:00:00 2001 From: baptou12 Date: Fri, 6 Oct 2023 10:23:19 +0200 Subject: [PATCH 033/188] chore: bump max gunicorn version up to `< 22.0` --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 07fd5c2c9c..543a0ab35f 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ api_requirements = [ "Flask >=2.2.3, < 3.0", "Flask-Cors >=3.0.10, < 4.0", - "gunicorn >=20.1.0, < 21.0", + "gunicorn >=21.0, < 22.0", "Werkzeug >=2.2.3, < 3.0", ] From 9aaeb6a08da45652849e77e2bfa29eef8210cfea Mon Sep 17 00:00:00 2001 From: baptou12 Date: Fri, 13 Oct 2023 10:39:08 +0200 Subject: [PATCH 034/188] bump version --- CHANGELOG.md | 10 +++++++++- setup.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f3d7d2789..fa4ded697d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 41.2.0 [#1199](https://github.com/openfisca/openfisca-core/pull/1199) + +#### Technical changes + +- Fix `openfisca-core` Web API error triggered by `Gunicorn` < 22.0. + - Bump `Gunicorn` major revision to fix error on Web API. + Source: https://github.com/benoitc/gunicorn/issues/2564 + ### 41.1.2 [#1192](https://github.com/openfisca/openfisca-core/pull/1192) #### Technical changes @@ -12,7 +20,7 @@ - Skip type-checking tasks - Before their definition was commented out but still run with `make test` - - Now they're skipped but not commented, which is needed to fix the + - Now they're skipped but not commented, which is needed to fix the underlying issues ## 41.1.0 [#1195](https://github.com/openfisca/openfisca-core/pull/1195) diff --git a/setup.py b/setup.py index 543a0ab35f..925d5115f0 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.1.2", + version="41.2.0", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From c5d7ca93019877adf4a3d8572958023fa1ba64eb Mon Sep 17 00:00:00 2001 From: Allan - CodeWorks Date: Thu, 9 Nov 2023 17:28:49 +0100 Subject: [PATCH 035/188] Add dunder contains and dunder iter to TracingParameterNodeAtInstant --- openfisca_core/tracers/tracing_parameter_node_at_instant.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openfisca_core/tracers/tracing_parameter_node_at_instant.py b/openfisca_core/tracers/tracing_parameter_node_at_instant.py index b18bc683ad..f618f59e97 100644 --- a/openfisca_core/tracers/tracing_parameter_node_at_instant.py +++ b/openfisca_core/tracers/tracing_parameter_node_at_instant.py @@ -36,6 +36,12 @@ def __getattr__( child = getattr(self.parameter_node_at_instant, key) return self.get_traced_child(child, key) + def __contains__(self, key) -> bool: + return key in self.parameter_node_at_instant + + def __iter__(self): + return iter(self.parameter_node_at_instant) + def __getitem__( self, key: Union[str, ArrayLike], From e26904afbc44c4d0ce78f698f1316f87c6b6d726 Mon Sep 17 00:00:00 2001 From: Allan - CodeWorks Date: Mon, 13 Nov 2023 10:14:26 +0100 Subject: [PATCH 036/188] Updates setup.py and changelog toward v41.3.0 --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa4ded697d..bc4aef54bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 41.3.0 [#1200](https://github.com/openfisca/openfisca-core/pull/1200) + +> As `TracingParameterNodeAtInstant` is a wrapper for `ParameterNodeAtInstant` +> which allows iteration and the use of `contains`, it was not possible +> to use those on a `TracingParameterNodeAtInstant` + +#### New features + +- Allows iterations on `TracingParameterNodeAtInstant` +- Allows keyword `contains` on `TracingParameterNodeAtInstant` + ## 41.2.0 [#1199](https://github.com/openfisca/openfisca-core/pull/1199) #### Technical changes diff --git a/setup.py b/setup.py index 925d5115f0..1722223da2 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.2.0", + version="41.3.0", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 6766bdca6857e144ac1517a12ea132352538c154 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 4 Oct 2023 18:51:45 +0200 Subject: [PATCH 037/188] Add documentation to get_projector_from_shortcut --- openfisca_core/entities/typing.py | 11 ++ openfisca_core/populations/__init__.py | 26 ++-- .../populations/group_population.py | 14 +- openfisca_core/populations/population.py | 24 ++-- openfisca_core/projectors/__init__.py | 21 ++- openfisca_core/projectors/helpers.py | 132 ++++++++++++++++-- openfisca_core/projectors/typing.py | 60 ++++++++ openfisca_core/simulations/__init__.py | 12 ++ openfisca_core/simulations/simulation.py | 97 ++++++------- .../simulations/simulation_builder.py | 66 ++++----- openfisca_tasks/test_code.mk | 1 + setup.cfg | 2 +- 12 files changed, 335 insertions(+), 131 deletions(-) create mode 100644 openfisca_core/projectors/typing.py diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py index 5a1e60d4ad..e1ae4a1ce9 100644 --- a/openfisca_core/entities/typing.py +++ b/openfisca_core/entities/typing.py @@ -1,5 +1,16 @@ +from __future__ import annotations + +from collections.abc import Iterable from typing import Protocol class Entity(Protocol): ... + + +class GroupEntity(Protocol): + ... + + +class Role(Protocol): + ... diff --git a/openfisca_core/populations/__init__.py b/openfisca_core/populations/__init__.py index e52a5bcaee..0047c528b6 100644 --- a/openfisca_core/populations/__init__.py +++ b/openfisca_core/populations/__init__.py @@ -21,17 +21,27 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from openfisca_core.projectors import ( # noqa: F401 +from openfisca_core.projectors import ( EntityToPersonProjector, FirstPersonToEntityProjector, Projector, UniqueRoleToEntityProjector, ) -from openfisca_core.projectors.helpers import ( # noqa: F401 - get_projector_from_shortcut, - projectable, -) +from openfisca_core.projectors.helpers import get_projector_from_shortcut, projectable + +from .config import ADD, DIVIDE +from .group_population import GroupPopulation +from .population import Population -from .config import ADD, DIVIDE # noqa: F401 -from .group_population import GroupPopulation # noqa: F401 -from .population import Population # noqa: F401 +__all__ = [ + "ADD", + "DIVIDE", + "EntityToPersonProjector", + "FirstPersonToEntityProjector", + "GroupPopulation", + "Population", + "Projector", + "UniqueRoleToEntityProjector", + "get_projector_from_shortcut", + "projectable", +] diff --git a/openfisca_core/populations/group_population.py b/openfisca_core/populations/group_population.py index 3ec47fe291..d77816face 100644 --- a/openfisca_core/populations/group_population.py +++ b/openfisca_core/populations/group_population.py @@ -2,9 +2,7 @@ import numpy -from openfisca_core import projectors -from openfisca_core.entities import Role -from openfisca_core.indexed_enums import EnumArray +from openfisca_core import entities, indexed_enums, projectors from .population import Population @@ -67,7 +65,7 @@ def members_role(self): return self._members_role @members_role.setter - def members_role(self, members_role: typing.Iterable[Role]): + def members_role(self, members_role: typing.Iterable[entities.Role]): if members_role is not None: self._members_role = numpy.array(list(members_role)) @@ -256,8 +254,8 @@ def value_from_person(self, array, role, default=0): self.members.check_array_compatible_with_entity(array) members_map = self.ordered_members_map result = self.filled_array(default, dtype=array.dtype) - if isinstance(array, EnumArray): - result = EnumArray(result, array.possible_values) + if isinstance(array, indexed_enums.EnumArray): + result = indexed_enums.EnumArray(result, array.possible_values) role_filter = self.members.has_role(role) entity_filter = self.any(role_filter) @@ -287,8 +285,8 @@ def value_nth_person(self, n, array, default=0): positions[members_map] == n ] - if isinstance(array, EnumArray): - result = EnumArray(result, array.possible_values) + if isinstance(array, indexed_enums.EnumArray): + result = indexed_enums.EnumArray(result, array.possible_values) return result diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index 4aa28b2ad9..fe1137d83b 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -8,9 +8,7 @@ import numpy -from openfisca_core import periods, projectors -from openfisca_core.holders import Holder, MemoryUsage -from openfisca_core.projectors import Projector +from openfisca_core import holders, periods, projectors from . import config @@ -18,7 +16,7 @@ class Population: simulation: Optional[Simulation] entity: Entity - _holders: Dict[str, Holder] + _holders: Dict[str, holders.Holder] count: int ids: Array[str] @@ -50,11 +48,11 @@ def filled_array( ) -> Union[Array[float], Array[bool]]: return numpy.full(self.count, value, dtype) - def __getattr__(self, attribute: str) -> Projector: - projector: Optional[Projector] + def __getattr__(self, attribute: str) -> projectors.Projector: + projector: Optional[projectors.Projector] projector = projectors.get_projector_from_shortcut(self, attribute) - if isinstance(projector, Projector): + if isinstance(projector, projectors.Projector): return projector raise AttributeError( @@ -159,13 +157,13 @@ def __call__( # Helpers - def get_holder(self, variable_name: str) -> Holder: + def get_holder(self, variable_name: str) -> holders.Holder: self.entity.check_variable_defined_for_entity(variable_name) holder = self._holders.get(variable_name) if holder: return holder variable = self.entity.get_variable(variable_name) - self._holders[variable_name] = holder = Holder(variable, self) + self._holders[variable_name] = holder = holders.Holder(variable, self) return holder def get_memory_usage( @@ -220,7 +218,7 @@ def has_role(self, role: Role) -> Optional[Array[bool]]: def value_from_partner( self, array: Array[float], - entity: Projector, + entity: projectors.Projector, role: Role, ) -> Optional[Array[float]]: self.check_array_compatible_with_entity(array) @@ -265,7 +263,9 @@ def get_rank( # If entity is for instance 'person.household', we get the reference entity 'household' behind the projector entity = ( - entity if not isinstance(entity, Projector) else entity.reference_entity + entity + if not isinstance(entity, projectors.Projector) + else entity.reference_entity ) positions = entity.members_position @@ -300,5 +300,5 @@ class Calculate(NamedTuple): class MemoryUsageByVariable(TypedDict, total=False): - by_variable: Dict[str, MemoryUsage] + by_variable: Dict[str, holders.MemoryUsage] total_nb_bytes: int diff --git a/openfisca_core/projectors/__init__.py b/openfisca_core/projectors/__init__.py index 9711808d0d..28776e3cf9 100644 --- a/openfisca_core/projectors/__init__.py +++ b/openfisca_core/projectors/__init__.py @@ -21,8 +21,19 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .entity_to_person_projector import EntityToPersonProjector # noqa: F401 -from .first_person_to_entity_projector import FirstPersonToEntityProjector # noqa: F401 -from .helpers import get_projector_from_shortcut, projectable # noqa: F401 -from .projector import Projector # noqa: F401 -from .unique_role_to_entity_projector import UniqueRoleToEntityProjector # noqa: F401 +from . import typing +from .entity_to_person_projector import EntityToPersonProjector +from .first_person_to_entity_projector import FirstPersonToEntityProjector +from .helpers import get_projector_from_shortcut, projectable +from .projector import Projector +from .unique_role_to_entity_projector import UniqueRoleToEntityProjector + +__all__ = [ + "EntityToPersonProjector", + "FirstPersonToEntityProjector", + "get_projector_from_shortcut", + "projectable", + "Projector", + "UniqueRoleToEntityProjector", + "typing", +] diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index ef205fd065..9e65226c46 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -1,4 +1,10 @@ -from openfisca_core import projectors +from __future__ import annotations + +from collections.abc import Mapping + +from openfisca_core import entities, projectors + +from .typing import Entity, GroupEntity, GroupPopulation, Population, Role, Simulation def projectable(function): @@ -10,25 +16,123 @@ def projectable(function): return function -def get_projector_from_shortcut(population, shortcut, parent=None): - if population.entity.is_person: - if shortcut in population.simulation.populations: - entity_2 = population.simulation.populations[shortcut] - return projectors.EntityToPersonProjector(entity_2, parent) - else: - if shortcut == "first_person": - return projectors.FirstPersonToEntityProjector(population, parent) - role = next( +def get_projector_from_shortcut( + population: Population | GroupPopulation, + shortcut: str, + parent: projectors.Projector | None = None, +) -> projectors.Projector | None: + """???. + + Args: + population: ??? + shortcut: ??? + parent: ??? + + Examples: + >>> from openfisca_core import ( + ... entities, + ... populations, + ... simulations, + ... taxbenefitsystems, + ... ) + + >>> entity_1 = entities.Entity("person", "", "", "") + + >>> entity_2 = entities.Entity("martian", "", "", "") + + >>> group_entity_1 = entities.GroupEntity("family", "", "", "", []) + + >>> roles = [ + ... {"key": "person"}, + ... {"key": "martian", "subroles": ["cat", "dog"]}, + ... ] + + >>> group_entity_2 = entities.GroupEntity("family", "", "", "", roles) + + >>> population = populations.Population(entity_1) + + >>> group_population_1 = populations.GroupPopulation(entity_2, []) + + >>> group_population_2 = populations.GroupPopulation(group_entity_1, []) + + >>> group_population_3 = populations.GroupPopulation(group_entity_2, []) + + >>> populations = { + ... entity_1.key: population, + ... entity_2.key: group_population_1, + ... group_entity_1.key: group_population_2, + ... group_entity_2.key: group_population_3, + ... } + + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem( + ... [entity_1, entity_2, group_entity_1, group_entity_2] + ... ) + + >>> simulation = simulations.Simulation(tax_benefit_system, populations) + + >>> get_projector_from_shortcut(population, "person") + <...EntityToPersonProjector object at ...> + + >>> get_projector_from_shortcut(population, "martian") + <...EntityToPersonProjector object at ...> + + >>> get_projector_from_shortcut(population, "family") + <...EntityToPersonProjector object at ...> + + >>> get_projector_from_shortcut(group_population_1, "person") + <...EntityToPersonProjector object at ...> + + >>> get_projector_from_shortcut(group_population_1, "martian") + <...EntityToPersonProjector object at ...> + + >>> get_projector_from_shortcut(group_population_1, "family") + <...EntityToPersonProjector object at ...> + + >>> get_projector_from_shortcut(group_population_2, "first_person") + <...FirstPersonToEntityProjector object at ...> + + >>> get_projector_from_shortcut(group_population_3, "first_person") + <...FirstPersonToEntityProjector object at ...> + + >>> get_projector_from_shortcut(group_population_3, "cat") + <...UniqueRoleToEntityProjector object at ...> + + >>> get_projector_from_shortcut(group_population_3, "dog") + <...UniqueRoleToEntityProjector object at ...> + + """ + + entity: Entity | GroupEntity = population.entity + + if isinstance(entity, entities.GroupEntity): + role: Role | None = next( ( role - for role in population.entity.flattened_roles + for role in entity.flattened_roles if (role.max == 1) and (role.key == shortcut) ), None, ) - if role: + + if role is not None: return projectors.UniqueRoleToEntityProjector(population, role, parent) - if shortcut in population.entity.containing_entities: - return getattr( + + if shortcut in entity.containing_entities: + projector: projectors.Projector = getattr( projectors.FirstPersonToEntityProjector(population, parent), shortcut ) + return projector + + if isinstance(entity, entities.Entity): + simulation: Simulation = population.simulation + populations: Mapping[str, Population | GroupPopulation] = simulation.populations + + if shortcut in populations.keys(): + return projectors.EntityToPersonProjector(populations[shortcut], parent) + + return None + + if shortcut == "first_person": + return projectors.FirstPersonToEntityProjector(population, parent) + + return None diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py new file mode 100644 index 0000000000..f3a67e5d6e --- /dev/null +++ b/openfisca_core/projectors/typing.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Iterable, Protocol + + +class Entity(Protocol): + @property + def is_person(self) -> bool: + ... + + +class GroupEntity(Protocol): + @property + def containing_entities(self) -> Iterable[str]: + ... + + @property + def is_person(self) -> bool: + ... + + @property + def flattened_roles(self) -> Iterable[Role]: + ... + + +class Role(Protocol): + @property + def key(self) -> str: + ... + + @property + def max(self) -> int | None: + ... + + +class Population(Protocol): + @property + def entity(self) -> Entity | GroupEntity: + ... + + @property + def simulation(self) -> Simulation: + ... + + +class GroupPopulation(Protocol): + @property + def entity(self) -> Entity | GroupEntity: + ... + + @property + def simulation(self) -> Simulation: + ... + + +class Simulation(Protocol): + @property + def populations(self) -> Mapping[str, Population | GroupPopulation]: + ... diff --git a/openfisca_core/simulations/__init__.py b/openfisca_core/simulations/__init__.py index 913e90d1ed..670b922ebb 100644 --- a/openfisca_core/simulations/__init__.py +++ b/openfisca_core/simulations/__init__.py @@ -35,3 +35,15 @@ ) from .simulation import Simulation # noqa: F401 from .simulation_builder import SimulationBuilder # noqa: F401 + +__all__ = [ + "CycleError", + "NaNCreationError", + "Simulation", + "SimulationBuilder", + "SpiralError", + "calculate_output_add", + "calculate_output_divide", + "check_type", + "transform_to_strict_syntax", +] diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index d501c4f915..304d7338ab 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -8,16 +8,8 @@ import numpy -from openfisca_core import commons, periods -from openfisca_core.errors import CycleError, SpiralError, VariableNotFoundError -from openfisca_core.indexed_enums import Enum, EnumArray -from openfisca_core.periods import DateUnit, Period -from openfisca_core.tracers import ( - FullTracer, - SimpleTracer, - TracingParameterNodeAtInstant, -) -from openfisca_core.warnings import TempfileWarning +from openfisca_core import commons, errors, indexed_enums, periods, tracers +from openfisca_core import warnings as core_warnings class Simulation: @@ -51,7 +43,7 @@ def __init__( self.debug = False self.trace = False - self.tracer = SimpleTracer() + self.tracer = tracers.SimpleTracer() self.opt_out_cache = False # controls the spirals detection; check for performance impact if > 1 @@ -67,9 +59,9 @@ def trace(self): def trace(self, trace): self._trace = trace if trace: - self.tracer = FullTracer() + self.tracer = tracers.FullTracer() else: - self.tracer = SimpleTracer() + self.tracer = tracers.SimpleTracer() def link_to_entities_instances(self): for _key, entity_instance in self.populations.items(): @@ -93,7 +85,9 @@ def data_storage_dir(self): ).format(self._data_storage_dir), "You should remove this directory once you're done with your simulation.", ] - warnings.warn(" ".join(message), TempfileWarning, stacklevel=2) + warnings.warn( + " ".join(message), core_warnings.TempfileWarning, stacklevel=2 + ) return self._data_storage_dir # ----- Calculation methods ----- # @@ -101,7 +95,7 @@ def data_storage_dir(self): def calculate(self, variable_name: str, period): """Calculate ``variable_name`` for ``period``.""" - if period is not None and not isinstance(period, Period): + if period is not None and not isinstance(period, periods.Period): period = periods.period(period) self.tracer.record_calculation_start(variable_name, period) @@ -115,7 +109,7 @@ def calculate(self, variable_name: str, period): self.tracer.record_calculation_end() self.purge_cache_of_invalid_values() - def _calculate(self, variable_name: str, period: Period): + def _calculate(self, variable_name: str, period: periods.Period): """ Calculate the variable ``variable_name`` for the period ``period``, using the variable formula if it exists. @@ -130,7 +124,7 @@ def _calculate(self, variable_name: str, period: Period): ) if variable is None: - raise VariableNotFoundError(variable_name, self.tax_benefit_system) + raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system) self._check_period_consistency(period, variable) @@ -153,7 +147,7 @@ def _calculate(self, variable_name: str, period: Period): array = self._cast_formula_result(array, variable) holder.put_in_cache(array, period) - except SpiralError: + except errors.SpiralError: array = holder.default_array() return array @@ -175,9 +169,9 @@ def calculate_add(self, variable_name: str, period): ) if variable is None: - raise VariableNotFoundError(variable_name, self.tax_benefit_system) + raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system) - if period is not None and not isinstance(period, Period): + if period is not None and not isinstance(period, periods.Period): period = periods.period(period) # Check that the requested period matches definition_period @@ -192,7 +186,7 @@ def calculate_add(self, variable_name: str, period): ) if variable.definition_period not in ( - DateUnit.isoformat + DateUnit.isocalendar + periods.DateUnit.isoformat + periods.DateUnit.isocalendar ): raise ValueError( f"Unable to ADD constant variable '{variable.name}' over " @@ -213,9 +207,9 @@ def calculate_divide(self, variable_name: str, period): ) if variable is None: - raise VariableNotFoundError(variable_name, self.tax_benefit_system) + raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system) - if period is not None and not isinstance(period, Period): + if period is not None and not isinstance(period, periods.Period): period = periods.period(period) if ( @@ -231,7 +225,7 @@ def calculate_divide(self, variable_name: str, period): ) if variable.definition_period not in ( - DateUnit.isoformat + DateUnit.isocalendar + periods.DateUnit.isoformat + periods.DateUnit.isocalendar ): raise ValueError( f"Unable to DIVIDE constant variable '{variable.name}' over " @@ -240,7 +234,8 @@ def calculate_divide(self, variable_name: str, period): ) if ( - period.unit not in (DateUnit.isoformat + DateUnit.isocalendar) + period.unit + not in (periods.DateUnit.isoformat + periods.DateUnit.isocalendar) or period.size != 1 ): raise ValueError( @@ -249,31 +244,31 @@ def calculate_divide(self, variable_name: str, period): "as a denominator to divide a variable over time." ) - if variable.definition_period == DateUnit.YEAR: + if variable.definition_period == periods.DateUnit.YEAR: calculation_period = period.this_year - elif variable.definition_period == DateUnit.MONTH: + elif variable.definition_period == periods.DateUnit.MONTH: calculation_period = period.first_month - elif variable.definition_period == DateUnit.DAY: + elif variable.definition_period == periods.DateUnit.DAY: calculation_period = period.first_day - elif variable.definition_period == DateUnit.WEEK: + elif variable.definition_period == periods.DateUnit.WEEK: calculation_period = period.first_week else: calculation_period = period.first_weekday - if period.unit == DateUnit.YEAR: + if period.unit == periods.DateUnit.YEAR: denominator = calculation_period.size_in_years - elif period.unit == DateUnit.MONTH: + elif period.unit == periods.DateUnit.MONTH: denominator = calculation_period.size_in_months - elif period.unit == DateUnit.DAY: + elif period.unit == periods.DateUnit.DAY: denominator = calculation_period.size_in_days - elif period.unit == DateUnit.WEEK: + elif period.unit == periods.DateUnit.WEEK: denominator = calculation_period.size_in_weeks else: @@ -293,7 +288,7 @@ def calculate_output(self, variable_name: str, period): ) if variable is None: - raise VariableNotFoundError(variable_name, self.tax_benefit_system) + raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system) if variable.calculate_output is None: return self.calculate(variable_name, period) @@ -301,7 +296,7 @@ def calculate_output(self, variable_name: str, period): return variable.calculate_output(self, variable_name, period) def trace_parameters_at_instant(self, formula_period): - return TracingParameterNodeAtInstant( + return tracers.TracingParameterNodeAtInstant( self.tax_benefit_system.get_parameters_at_instant(formula_period), self.tracer, ) @@ -331,10 +326,13 @@ def _check_period_consistency(self, period, variable): """ Check that a period matches the variable definition_period """ - if variable.definition_period == DateUnit.ETERNITY: + if variable.definition_period == periods.DateUnit.ETERNITY: return # For variables which values are constant in time, all periods are accepted - if variable.definition_period == DateUnit.YEAR and period.unit != DateUnit.YEAR: + if ( + variable.definition_period == periods.DateUnit.YEAR + and period.unit != periods.DateUnit.YEAR + ): raise ValueError( "Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole year. You can use the DIVIDE option to get an estimate of {0} by dividing the yearly value by 12, or change the requested period to 'period.this_year'.".format( variable.name, period @@ -342,8 +340,8 @@ def _check_period_consistency(self, period, variable): ) if ( - variable.definition_period == DateUnit.MONTH - and period.unit != DateUnit.MONTH + variable.definition_period == periods.DateUnit.MONTH + and period.unit != periods.DateUnit.MONTH ): raise ValueError( "Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole month. You can use the ADD option to sum '{0}' over the requested period, or change the requested period to 'period.first_month'.".format( @@ -351,7 +349,10 @@ def _check_period_consistency(self, period, variable): ) ) - if variable.definition_period == DateUnit.WEEK and period.unit != DateUnit.WEEK: + if ( + variable.definition_period == periods.DateUnit.WEEK + and period.unit != periods.DateUnit.WEEK + ): raise ValueError( "Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole week. You can use the ADD option to sum '{0}' over the requested period, or change the requested period to 'period.first_week'.".format( variable.name, period @@ -366,7 +367,9 @@ def _check_period_consistency(self, period, variable): ) def _cast_formula_result(self, value, variable): - if variable.value_type == Enum and not isinstance(value, EnumArray): + if variable.value_type == indexed_enums.Enum and not isinstance( + value, indexed_enums.EnumArray + ): return variable.possible_values.encode(value) if not isinstance(value, numpy.ndarray): @@ -394,7 +397,7 @@ def _check_for_cycle(self, variable: str, period): if frame["name"] == variable ] if period in previous_periods: - raise CycleError( + raise errors.CycleError( "Circular definition detected on formula {}@{}".format(variable, period) ) spiral = len(previous_periods) >= self.max_spiral_loops @@ -403,7 +406,7 @@ def _check_for_cycle(self, variable: str, period): message = "Quasicircular definition detected on formula {}@{} involving {}".format( variable, period, self.tracer.stack ) - raise SpiralError(message, variable) + raise errors.SpiralError(message, variable) def invalidate_cache_entry(self, variable: str, period): self.invalidated_caches.add(Cache(variable, period)) @@ -429,7 +432,7 @@ def get_array(self, variable_name: str, period): Unlike :meth:`.calculate`, this method *does not* trigger calculations and *does not* use any formula. """ - if period is not None and not isinstance(period, Period): + if period is not None and not isinstance(period, periods.Period): period = periods.period(period) return self.get_holder(variable_name).get_array(period) @@ -520,7 +523,7 @@ def set_input(self, variable_name: str, period, value): ) if variable is None: - raise VariableNotFoundError(variable_name, self.tax_benefit_system) + raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system) period = periods.period(period) if (variable.end is not None) and (period.start.date > variable.end): @@ -535,7 +538,7 @@ def get_variable_population(self, variable_name: str) -> Population: ) if variable is None: - raise VariableNotFoundError(variable_name, self.tax_benefit_system) + raise errors.VariableNotFoundError(variable_name, self.tax_benefit_system) return self.populations[variable.entity.key] @@ -592,4 +595,4 @@ def clone(self, debug=False, trace=False): class Cache(NamedTuple): variable: str - period: Period + period: periods.Period diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 71c37a3524..d48ac0152e 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -5,16 +5,10 @@ import dpath.util import numpy -from openfisca_core import periods -from openfisca_core.entities import Entity -from openfisca_core.errors import ( - PeriodMismatchError, - SituationParsingError, - VariableNotFoundError, -) -from openfisca_core.populations import Population -from openfisca_core.simulations import Simulation, helpers -from openfisca_core.variables import Variable +from openfisca_core import entities, errors, periods, populations, variables + +from . import helpers +from .simulation import Simulation class SimulationBuilder: @@ -28,25 +22,25 @@ def __init__(self): # JSON input - Memory of known input values. Indexed by variable or axis name. self.input_buffer: Dict[ - Variable.name, Dict[str(periods.period), numpy.array] + variables.Variable.name, Dict[str(periods.period), numpy.array] ] = {} - self.populations: Dict[Entity.key, Population] = {} + self.populations: Dict[entities.Entity.key, populations.Population] = {} # JSON input - Number of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_ids``, including axes. - self.entity_counts: Dict[Entity.plural, int] = {} + self.entity_counts: Dict[entities.Entity.plural, int] = {} # JSON input - List of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_counts``. - self.entity_ids: Dict[Entity.plural, List[int]] = {} + self.entity_ids: Dict[entities.Entity.plural, List[int]] = {} # Links entities with persons. For each person index in persons ids list, set entity index in entity ids id. E.g.: self.memberships[entity.plural][person_index] = entity_ids.index(instance_id) - self.memberships: Dict[Entity.plural, List[int]] = {} - self.roles: Dict[Entity.plural, List[int]] = {} + self.memberships: Dict[entities.Entity.plural, List[int]] = {} + self.roles: Dict[entities.Entity.plural, List[int]] = {} - self.variable_entities: Dict[Variable.name, Entity] = {} + self.variable_entities: Dict[variables.Variable.name, entities.Entity] = {} self.axes = [[]] - self.axes_entity_counts: Dict[Entity.plural, int] = {} - self.axes_entity_ids: Dict[Entity.plural, List[int]] = {} - self.axes_memberships: Dict[Entity.plural, List[int]] = {} - self.axes_roles: Dict[Entity.plural, List[int]] = {} + self.axes_entity_counts: Dict[entities.Entity.plural, int] = {} + self.axes_entity_ids: Dict[entities.Entity.plural, List[int]] = {} + self.axes_memberships: Dict[entities.Entity.plural, List[int]] = {} + self.axes_roles: Dict[entities.Entity.plural, List[int]] = {} def build_from_dict(self, tax_benefit_system, input_dict): """ @@ -99,7 +93,7 @@ def build_from_entities(self, tax_benefit_system, input_dict): ] if unexpected_entities: unexpected_entity = unexpected_entities[0] - raise SituationParsingError( + raise errors.SituationParsingError( [unexpected_entity], "".join( [ @@ -115,7 +109,7 @@ def build_from_entities(self, tax_benefit_system, input_dict): persons_json = input_dict.get(tax_benefit_system.person_entity.plural, None) if not persons_json: - raise SituationParsingError( + raise errors.SituationParsingError( [tax_benefit_system.person_entity.plural], "No {0} found. At least one {0} must be defined to run a simulation.".format( tax_benefit_system.person_entity.key @@ -139,14 +133,14 @@ def build_from_entities(self, tax_benefit_system, input_dict): try: self.finalize_variables_init(simulation.persons) - except PeriodMismatchError as e: + except errors.PeriodMismatchError as e: self.raise_period_mismatch(simulation.persons.entity, persons_json, e) for entity_class in tax_benefit_system.group_entities: try: population = simulation.populations[entity_class.key] self.finalize_variables_init(population) - except PeriodMismatchError as e: + except errors.PeriodMismatchError as e: self.raise_period_mismatch(population.entity, instances_json, e) return simulation @@ -168,7 +162,7 @@ def build_from_variables(self, tax_benefit_system, input_dict): for variable, value in input_dict.items(): if not isinstance(value, dict): if self.default_period is None: - raise SituationParsingError( + raise errors.SituationParsingError( [variable], "Can't deal with type: expected object. Input variables should be set for specific periods. For instance: {'salary': {'2017-01': 2000, '2017-02': 2500}}, or {'birth_date': {'ETERNITY': '1980-01-01'}}.", ) @@ -358,7 +352,7 @@ def add_group_entity(self, persons_plural, persons_ids, entity, instances_json): role = role_by_plural[role_plural] if role.max is not None and len(persons_with_role) > role.max: - raise SituationParsingError( + raise errors.SituationParsingError( [entity.plural, instance_id, role_plural], f"There can be at most {role.max} {role_plural} in a {entity.key}. {len(persons_with_role)} were declared in '{instance_id}'.", ) @@ -413,14 +407,14 @@ def check_persons_to_allocate( person_id, str, [entity_plural, entity_id, role_id, str(index)] ) if person_id not in persons_ids: - raise SituationParsingError( + raise errors.SituationParsingError( [entity_plural, entity_id, role_id], "Unexpected value: {0}. {0} has been declared in {1} {2}, but has not been declared in {3}.".format( person_id, entity_id, role_id, persons_plural ), ) if person_id not in persons_to_allocate: - raise SituationParsingError( + raise errors.SituationParsingError( [entity_plural, entity_id, role_id], "{} has been declared more than once in {}".format( person_id, entity_plural @@ -433,15 +427,15 @@ def init_variable_values(self, entity, instance_object, instance_id): try: entity.check_variable_defined_for_entity(variable_name) except ValueError as e: # The variable is defined for another entity - raise SituationParsingError(path_in_json, e.args[0]) - except VariableNotFoundError as e: # The variable doesn't exist - raise SituationParsingError(path_in_json, str(e), code=404) + raise errors.SituationParsingError(path_in_json, e.args[0]) + except errors.VariableNotFoundError as e: # The variable doesn't exist + raise errors.SituationParsingError(path_in_json, str(e), code=404) instance_index = self.get_ids(entity.plural).index(instance_id) if not isinstance(variable_values, dict): if self.default_period is None: - raise SituationParsingError( + raise errors.SituationParsingError( path_in_json, "Can't deal with type: expected object. Input variables should be set for specific periods. For instance: {'salary': {'2017-01': 2000, '2017-02': 2500}}, or {'birth_date': {'ETERNITY': '1980-01-01'}}.", ) @@ -451,7 +445,7 @@ def init_variable_values(self, entity, instance_object, instance_id): try: periods.period(period_str) except ValueError as e: - raise SituationParsingError(path_in_json, e.args[0]) + raise errors.SituationParsingError(path_in_json, e.args[0]) variable = entity.get_variable(variable_name) self.add_variable_value( entity, variable, instance_index, instance_id, period_str, value @@ -474,7 +468,7 @@ def add_variable_value( try: value = variable.check_set_value(value) except ValueError as error: - raise SituationParsingError(path_in_json, *error.args) + raise errors.SituationParsingError(path_in_json, *error.args) array[instance_index] = value @@ -530,7 +524,7 @@ def raise_period_mismatch(self, entity, json, e): entity.plural ] # Fallback: if we can't find the culprit, just set the error at the entities level - raise SituationParsingError(path, e.message) + raise errors.SituationParsingError(path, e.message) # Returns the total number of instances of this entity, including when there is replication along axes def get_count(self, entity_name): diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index 33bccf7a76..2734d20275 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -36,6 +36,7 @@ test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 openfisca_core/entities \ openfisca_core/holders \ openfisca_core/periods \ + openfisca_core/projectors \ openfisca_core/types @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ coverage run -m \ diff --git a/setup.cfg b/setup.cfg index 9740d15ff8..07fc80f4ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ extend-ignore = D ignore = E203, E501, F405, RST301, W503 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/types +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors openfisca_core/types max-line-length = 88 per-file-ignores = */typing.py:D101,D102,E704, */__init__.py:F401 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged From 356ad5af9ca0d7802773ada9ab66fc4e4b6c9766 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 4 Oct 2023 19:39:03 +0200 Subject: [PATCH 038/188] Fix failing tests --- openfisca_core/entities/typing.py | 1 - openfisca_core/projectors/helpers.py | 32 +++++++++++++++++----------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py index e1ae4a1ce9..93b9946ffc 100644 --- a/openfisca_core/entities/typing.py +++ b/openfisca_core/entities/typing.py @@ -1,6 +1,5 @@ from __future__ import annotations -from collections.abc import Iterable from typing import Protocol diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index 9e65226c46..f3544cf28a 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -47,7 +47,7 @@ def get_projector_from_shortcut( ... {"key": "martian", "subroles": ["cat", "dog"]}, ... ] - >>> group_entity_2 = entities.GroupEntity("family", "", "", "", roles) + >>> group_entity_2 = entities.GroupEntity("household", "", "", "", roles) >>> population = populations.Population(entity_1) @@ -79,6 +79,9 @@ def get_projector_from_shortcut( >>> get_projector_from_shortcut(population, "family") <...EntityToPersonProjector object at ...> + >>> get_projector_from_shortcut(population, "household") + <...EntityToPersonProjector object at ...> + >>> get_projector_from_shortcut(group_population_1, "person") <...EntityToPersonProjector object at ...> @@ -88,6 +91,9 @@ def get_projector_from_shortcut( >>> get_projector_from_shortcut(group_population_1, "family") <...EntityToPersonProjector object at ...> + >>> get_projector_from_shortcut(group_population_1, "household") + <...EntityToPersonProjector object at ...> + >>> get_projector_from_shortcut(group_population_2, "first_person") <...FirstPersonToEntityProjector object at ...> @@ -104,6 +110,18 @@ def get_projector_from_shortcut( entity: Entity | GroupEntity = population.entity + if not isinstance(entity, entities.GroupEntity): + simulation: Simulation = population.simulation + populations: Mapping[str, Population | GroupPopulation] = simulation.populations + + if shortcut in populations.keys(): + return projectors.EntityToPersonProjector(populations[shortcut], parent) + + return None + + if shortcut == "first_person": + return projectors.FirstPersonToEntityProjector(population, parent) + if isinstance(entity, entities.GroupEntity): role: Role | None = next( ( @@ -123,16 +141,4 @@ def get_projector_from_shortcut( ) return projector - if isinstance(entity, entities.Entity): - simulation: Simulation = population.simulation - populations: Mapping[str, Population | GroupPopulation] = simulation.populations - - if shortcut in populations.keys(): - return projectors.EntityToPersonProjector(populations[shortcut], parent) - - return None - - if shortcut == "first_person": - return projectors.FirstPersonToEntityProjector(population, parent) - return None From 6d6ca102cfce6363f888c1683f61e8267da5971d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 5 Oct 2023 04:43:16 +0200 Subject: [PATCH 039/188] Factor out find_role --- openfisca_core/entities/__init__.py | 4 +- openfisca_core/entities/helpers.py | 75 ++++++++++++++++++++++++++++ openfisca_core/entities/typing.py | 11 +++- openfisca_core/projectors/helpers.py | 27 +++++----- openfisca_core/projectors/typing.py | 8 +-- openfisca_tasks/lint.mk | 2 +- 6 files changed, 101 insertions(+), 26 deletions(-) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index c90b9d0d6b..2ba382b7d8 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -24,7 +24,7 @@ from . import typing from .entity import Entity from .group_entity import GroupEntity -from .helpers import build_entity +from .helpers import build_entity, find_role from .role import Role -__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "typing"] +__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "find_role", "typing"] diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 43969fcd7a..c25f0fbd19 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,5 +1,8 @@ +from __future__ import annotations + from .entity import Entity from .group_entity import GroupEntity +from .typing import Role def build_entity( @@ -18,3 +21,75 @@ def build_entity( return GroupEntity( key, plural, label, doc, roles, containing_entities=containing_entities ) + + +def find_role( + group_entity: GroupEntity, key: str, *, total: int | None = None +) -> Role | None: + """Find a role in an entity. + + Args: + group_entity (GroupEntity): The entity to search in. + key (str): The key of the role to find. Defaults to `None`. + total (int | None): The `max` attribute of the role to find. + + Returns: + Role | None: The role if found, else `None`. + + Examples: + >>> from openfisca_core.entities.typing import RoleParams + + >>> principal = RoleParams( + ... key="principal", + ... label="Principal", + ... doc="Person focus of a calculation in a family context.", + ... max=1, + ... ) + + >>> partner = RoleParams( + ... key="partner", + ... plural="partners", + ... label="Partners", + ... doc="Persons partners of the principal.", + ... ) + + >>> parent = RoleParams( + ... key="parent", + ... plural="parents", + ... label="Parents", + ... doc="Persons parents of children of the principal", + ... subroles=["first_parent", "second_parent"], + ... ) + + >>> group_entity = build_entity( + ... key="family", + ... plural="families", + ... label="Family", + ... doc="A Family represents a collection of related persons.", + ... roles=[principal, partner, parent], + ... ) + + >>> find_role(group_entity, "principal", total=1) + Role(principal) + + >>> find_role(group_entity, "partner") + Role(partner) + + >>> find_role(group_entity, "parent", total=2) + Role(parent) + + >>> find_role(group_entity, "first_parent", total=1) + Role(first_parent) + + """ + + for role in group_entity.roles: + if role.subroles: + for subrole in role.subroles: + if (subrole.max == total) and (subrole.key == key): + return subrole + + if (role.max == total) and (role.key == key): + return role + + return None diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py index 93b9946ffc..2bb983a68f 100644 --- a/openfisca_core/entities/typing.py +++ b/openfisca_core/entities/typing.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Protocol +from typing import Protocol, TypedDict class Entity(Protocol): @@ -13,3 +13,12 @@ class GroupEntity(Protocol): class Role(Protocol): ... + + +class RoleParams(TypedDict, total=False): + key: str + plural: str + label: str + doc: str + max: int + subroles: list[str] diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index f3544cf28a..4247320f7a 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -43,7 +43,7 @@ def get_projector_from_shortcut( >>> group_entity_1 = entities.GroupEntity("family", "", "", "", []) >>> roles = [ - ... {"key": "person"}, + ... {"key": "person", "max": 1}, ... {"key": "martian", "subroles": ["cat", "dog"]}, ... ] @@ -100,6 +100,9 @@ def get_projector_from_shortcut( >>> get_projector_from_shortcut(group_population_3, "first_person") <...FirstPersonToEntityProjector object at ...> + >>> get_projector_from_shortcut(group_population_3, "person") + <...UniqueRoleToEntityProjector object at ...> + >>> get_projector_from_shortcut(group_population_3, "cat") <...UniqueRoleToEntityProjector object at ...> @@ -110,27 +113,21 @@ def get_projector_from_shortcut( entity: Entity | GroupEntity = population.entity - if not isinstance(entity, entities.GroupEntity): - simulation: Simulation = population.simulation - populations: Mapping[str, Population | GroupPopulation] = simulation.populations + if isinstance(entity, entities.Entity) and entity.is_person: + populations: Mapping[ + str, Population | GroupPopulation + ] = population.simulation.populations - if shortcut in populations.keys(): - return projectors.EntityToPersonProjector(populations[shortcut], parent) + if shortcut not in populations.keys(): + return None - return None + return projectors.EntityToPersonProjector(populations[shortcut], parent) if shortcut == "first_person": return projectors.FirstPersonToEntityProjector(population, parent) if isinstance(entity, entities.GroupEntity): - role: Role | None = next( - ( - role - for role in entity.flattened_roles - if (role.max == 1) and (role.key == shortcut) - ), - None, - ) + role: Role | None = entities.find_role(entity, shortcut, total=1) if role is not None: return projectors.UniqueRoleToEntityProjector(population, role, parent) diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py index f3a67e5d6e..0d8ca60351 100644 --- a/openfisca_core/projectors/typing.py +++ b/openfisca_core/projectors/typing.py @@ -25,13 +25,7 @@ def flattened_roles(self) -> Iterable[Role]: class Role(Protocol): - @property - def key(self) -> str: - ... - - @property - def max(self) -> int | None: - ... + ... class Population(Protocol): diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 7b958dd1e9..c937dd4d0b 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -36,7 +36,7 @@ lint-doc-%: ## Run static type checkers for type errors. check-types: @$(call print_help,$@:) - @mypy openfisca_core/entities + @mypy openfisca_core/entities openfisca_core/projectors @$(call print_pass,$@:) ## Run code formatters to correct style errors. From d89ad3a4eeadc0f1e4a436f59bfe7d8a35054d51 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 5 Oct 2023 05:18:03 +0200 Subject: [PATCH 040/188] Fix typing --- openfisca_core/entities/group_entity.py | 10 ++++++---- openfisca_core/entities/helpers.py | 16 +++++++++------- openfisca_core/entities/role.py | 4 ++-- openfisca_core/entities/typing.py | 8 +++++++- openfisca_core/projectors/helpers.py | 4 ++-- openfisca_core/projectors/typing.py | 8 ++++++-- 6 files changed, 32 insertions(+), 18 deletions(-) diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index 0c787da5b9..c8c1b28f1c 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections.abc import Iterable, Mapping from typing import Any @@ -36,17 +38,17 @@ def __init__( ) -> None: super().__init__(key, plural, label, doc) self.roles_description = roles - self.roles = [] + self.roles: Iterable[Role] = () for role_description in roles: role = Role(role_description, self) setattr(self, role.key.upper(), role) - self.roles.append(role) + self.roles = (*self.roles, role) if role_description.get("subroles"): - role.subroles = [] + role.subroles = () for subrole_key in role_description["subroles"]: subrole = Role({"key": subrole_key, "max": 1}, self) setattr(self, subrole.key.upper(), subrole) - role.subroles.append(subrole) + role.subroles = (*role.subroles, subrole) role.max = len(role.subroles) self.flattened_roles = tuple( chain.from_iterable(role.subroles or [role] for role in self.roles) diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index c25f0fbd19..91131c3903 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,8 +1,10 @@ from __future__ import annotations +from typing import Iterable + from .entity import Entity from .group_entity import GroupEntity -from .typing import Role +from .role import Role def build_entity( @@ -24,7 +26,7 @@ def build_entity( def find_role( - group_entity: GroupEntity, key: str, *, total: int | None = None + roles: Iterable[Role], key: str, *, total: int | None = None ) -> Role | None: """Find a role in an entity. @@ -69,21 +71,21 @@ def find_role( ... roles=[principal, partner, parent], ... ) - >>> find_role(group_entity, "principal", total=1) + >>> find_role(group_entity.roles, "principal", total=1) Role(principal) - >>> find_role(group_entity, "partner") + >>> find_role(group_entity.roles, "partner") Role(partner) - >>> find_role(group_entity, "parent", total=2) + >>> find_role(group_entity.roles, "parent", total=2) Role(parent) - >>> find_role(group_entity, "first_parent", total=1) + >>> find_role(group_entity.roles, "first_parent", total=1) Role(first_parent) """ - for role in group_entity.roles: + for role in roles: if role.subroles: for subrole in role.subroles: if (subrole.max == total) and (subrole.key == key): diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 976711b609..6bae50bc25 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Mapping, Sequence +from collections.abc import Iterable, Mapping from typing import Any import dataclasses @@ -57,7 +57,7 @@ class Role: max: int | None = None #: A list of subroles. - subroles: Sequence[Role] | None = None + subroles: Iterable[Role] | None = None @property def key(self) -> str: diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py index 2bb983a68f..1a2fb06b2c 100644 --- a/openfisca_core/entities/typing.py +++ b/openfisca_core/entities/typing.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Iterable from typing import Protocol, TypedDict @@ -12,7 +13,12 @@ class GroupEntity(Protocol): class Role(Protocol): - ... + max: int | None + subroles: Iterable[Role] | None + + @property + def key(self) -> str: + ... class RoleParams(TypedDict, total=False): diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index 4247320f7a..3c3c5f13e5 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -4,7 +4,7 @@ from openfisca_core import entities, projectors -from .typing import Entity, GroupEntity, GroupPopulation, Population, Role, Simulation +from .typing import Entity, GroupEntity, GroupPopulation, Population def projectable(function): @@ -127,7 +127,7 @@ def get_projector_from_shortcut( return projectors.FirstPersonToEntityProjector(population, parent) if isinstance(entity, entities.GroupEntity): - role: Role | None = entities.find_role(entity, shortcut, total=1) + role: entities.Role | None = entities.find_role(entity.roles, shortcut, total=1) if role is not None: return projectors.UniqueRoleToEntityProjector(population, role, parent) diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py index 0d8ca60351..5093f28e96 100644 --- a/openfisca_core/projectors/typing.py +++ b/openfisca_core/projectors/typing.py @@ -23,6 +23,10 @@ def is_person(self) -> bool: def flattened_roles(self) -> Iterable[Role]: ... + @property + def roles(self) -> Iterable[Role]: + ... + class Role(Protocol): ... @@ -30,7 +34,7 @@ class Role(Protocol): class Population(Protocol): @property - def entity(self) -> Entity | GroupEntity: + def entity(self) -> Entity: ... @property @@ -40,7 +44,7 @@ def simulation(self) -> Simulation: class GroupPopulation(Protocol): @property - def entity(self) -> Entity | GroupEntity: + def entity(self) -> GroupEntity: ... @property From 6e78af595e9f39a9b93265a35977c065a5342437 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 5 Oct 2023 17:50:52 +0200 Subject: [PATCH 041/188] Extract core entity to facilitate checks --- openfisca_core/entities/_core_entity.py | 69 +++++++++++++++++++++++++ openfisca_core/entities/entity.py | 42 +-------------- openfisca_core/entities/group_entity.py | 11 ++-- openfisca_core/projectors/helpers.py | 47 +++++------------ openfisca_core/variables/variable.py | 7 +-- 5 files changed, 95 insertions(+), 81 deletions(-) create mode 100644 openfisca_core/entities/_core_entity.py diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py new file mode 100644 index 0000000000..a578e656b0 --- /dev/null +++ b/openfisca_core/entities/_core_entity.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from abc import abstractmethod +from openfisca_core.types import TaxBenefitSystem, Variable +from typing import Any + +import os + +from .role import Role +from .typing import Entity + + +class _CoreEntity: + """Base class to build entities from.""" + + #: A key to identify the entity. + key: str + + #: The ``key``, pluralised. + plural: str | None + + #: A summary description. + label: str | None + + #: A full description. + doc: str | None + + #: Whether the entity is a person or not. + is_person: bool + + #: A TaxBenefitSystem instance. + _tax_benefit_system: TaxBenefitSystem | None = None + + @abstractmethod + def __init__(self, key: str, plural: str, label: str, doc: str, *args: Any) -> None: + ... + + def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystem) -> None: + self._tax_benefit_system = tax_benefit_system + + def get_variable( + self, + variable_name: str, + check_existence: bool = False, + ) -> Variable | None: + return self._tax_benefit_system.get_variable(variable_name, check_existence) + + def check_variable_defined_for_entity(self, variable_name: str) -> None: + variable: Variable | None + entity: Entity + + variable = self.get_variable(variable_name, check_existence=True) + + if variable is not None: + entity = variable.entity + + if entity.key != self.key: + message = ( + f"You tried to compute the variable '{variable_name}' for", + f"the entity '{self.plural}'; however the variable", + f"'{variable_name}' is defined for '{entity.plural}'.", + "Learn more about entities in our documentation:", + ".", + ) + raise ValueError(os.linesep.join(message)) + + def check_role_validity(self, role: Any) -> None: + if role is not None and not isinstance(role, Role): + raise ValueError(f"{role} is not a valid role") diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 2501cec80c..ea2deb505d 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,13 +1,9 @@ -from openfisca_core.types import TaxBenefitSystem, Variable -from typing import Any, Optional - -import os import textwrap -from .role import Role +from ._core_entity import _CoreEntity -class Entity: +class Entity(_CoreEntity): """ Represents an entity (e.g. a person, a household, etc.) on which calculations can be run. """ @@ -18,37 +14,3 @@ def __init__(self, key: str, plural: str, label: str, doc: str) -> None: self.plural = plural self.doc = textwrap.dedent(doc) self.is_person = True - self._tax_benefit_system = None - - def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystem): - self._tax_benefit_system = tax_benefit_system - - def check_role_validity(self, role: Any) -> None: - if role is not None and not isinstance(role, Role): - raise ValueError(f"{role} is not a valid role") - - def get_variable( - self, - variable_name: str, - check_existence: bool = False, - ) -> Optional[Variable]: - return self._tax_benefit_system.get_variable(variable_name, check_existence) - - def check_variable_defined_for_entity(self, variable_name: str) -> None: - variable: Optional[Variable] - entity: Entity - - variable = self.get_variable(variable_name, check_existence=True) - - if variable is not None: - entity = variable.entity - - if entity.key != self.key: - message = ( - f"You tried to compute the variable '{variable_name}' for", - f"the entity '{self.plural}'; however the variable", - f"'{variable_name}' is defined for '{entity.plural}'.", - "Learn more about entities in our documentation:", - ".", - ) - raise ValueError(os.linesep.join(message)) diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index c8c1b28f1c..eeae52d38f 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -3,13 +3,14 @@ from collections.abc import Iterable, Mapping from typing import Any +import textwrap from itertools import chain -from .entity import Entity +from ._core_entity import _CoreEntity from .role import Role -class GroupEntity(Entity): +class GroupEntity(_CoreEntity): """Represents an entity containing several others with different roles. A :class:`.GroupEntity` represents an :class:`.Entity` containing @@ -36,7 +37,11 @@ def __init__( roles: Iterable[Mapping[str, Any]], containing_entities: Iterable[str] = (), ) -> None: - super().__init__(key, plural, label, doc) + self.key = key + self.label = label + self.plural = plural + self.doc = textwrap.dedent(doc) + self.is_person = False self.roles_description = roles self.roles: Iterable[Role] = () for role_description in roles: diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index 3c3c5f13e5..e4fd4fc633 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -36,9 +36,7 @@ def get_projector_from_shortcut( ... taxbenefitsystems, ... ) - >>> entity_1 = entities.Entity("person", "", "", "") - - >>> entity_2 = entities.Entity("martian", "", "", "") + >>> entity = entities.Entity("person", "", "", "") >>> group_entity_1 = entities.GroupEntity("family", "", "", "", []) @@ -49,23 +47,20 @@ def get_projector_from_shortcut( >>> group_entity_2 = entities.GroupEntity("household", "", "", "", roles) - >>> population = populations.Population(entity_1) - - >>> group_population_1 = populations.GroupPopulation(entity_2, []) + >>> population = populations.Population(entity) - >>> group_population_2 = populations.GroupPopulation(group_entity_1, []) + >>> group_population_1 = populations.GroupPopulation(group_entity_1, []) - >>> group_population_3 = populations.GroupPopulation(group_entity_2, []) + >>> group_population_2 = populations.GroupPopulation(group_entity_2, []) >>> populations = { - ... entity_1.key: population, - ... entity_2.key: group_population_1, - ... group_entity_1.key: group_population_2, - ... group_entity_2.key: group_population_3, + ... entity.key: population, + ... group_entity_1.key: group_population_1, + ... group_entity_2.key: group_population_2, ... } >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem( - ... [entity_1, entity_2, group_entity_1, group_entity_2] + ... [entity, group_entity_1, group_entity_2] ... ) >>> simulation = simulations.Simulation(tax_benefit_system, populations) @@ -73,47 +68,29 @@ def get_projector_from_shortcut( >>> get_projector_from_shortcut(population, "person") <...EntityToPersonProjector object at ...> - >>> get_projector_from_shortcut(population, "martian") - <...EntityToPersonProjector object at ...> - >>> get_projector_from_shortcut(population, "family") <...EntityToPersonProjector object at ...> >>> get_projector_from_shortcut(population, "household") <...EntityToPersonProjector object at ...> - >>> get_projector_from_shortcut(group_population_1, "person") - <...EntityToPersonProjector object at ...> - - >>> get_projector_from_shortcut(group_population_1, "martian") - <...EntityToPersonProjector object at ...> - - >>> get_projector_from_shortcut(group_population_1, "family") - <...EntityToPersonProjector object at ...> - - >>> get_projector_from_shortcut(group_population_1, "household") - <...EntityToPersonProjector object at ...> - >>> get_projector_from_shortcut(group_population_2, "first_person") <...FirstPersonToEntityProjector object at ...> - >>> get_projector_from_shortcut(group_population_3, "first_person") - <...FirstPersonToEntityProjector object at ...> - - >>> get_projector_from_shortcut(group_population_3, "person") + >>> get_projector_from_shortcut(group_population_2, "person") <...UniqueRoleToEntityProjector object at ...> - >>> get_projector_from_shortcut(group_population_3, "cat") + >>> get_projector_from_shortcut(group_population_2, "cat") <...UniqueRoleToEntityProjector object at ...> - >>> get_projector_from_shortcut(group_population_3, "dog") + >>> get_projector_from_shortcut(group_population_2, "dog") <...UniqueRoleToEntityProjector object at ...> """ entity: Entity | GroupEntity = population.entity - if isinstance(entity, entities.Entity) and entity.is_person: + if isinstance(entity, entities.Entity): populations: Mapping[ str, Population | GroupPopulation ] = population.simulation.populations diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 7fb70dee61..2693a31211 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -11,7 +11,7 @@ import sortedcontainers from openfisca_core import periods, tools -from openfisca_core.entities import Entity +from openfisca_core.entities import Entity, GroupEntity from openfisca_core.indexed_enums import Enum, EnumArray from openfisca_core.periods import DateUnit, Period @@ -222,9 +222,10 @@ def set( return value def set_entity(self, entity): - if not isinstance(entity, Entity): + if not isinstance(entity, (Entity, GroupEntity)): raise ValueError( - f"Invalid value '{entity}' for attribute 'entity' in variable '{self.name}'. Must be an instance of Entity." + f"Invalid value '{entity}' for attribute 'entity' in variable " + f"'{self.name}'. Must be an instance of Entity or GroupEntity." ) return entity From bb4eaa820cc82990c15822d2fb6934a6c0097690 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 5 Oct 2023 18:07:31 +0200 Subject: [PATCH 042/188] Document function --- openfisca_core/projectors/helpers.py | 32 ++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index e4fd4fc633..4269ad6387 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -21,12 +21,32 @@ def get_projector_from_shortcut( shortcut: str, parent: projectors.Projector | None = None, ) -> projectors.Projector | None: - """???. - - Args: - population: ??? - shortcut: ??? - parent: ??? + """Get a projector from a shortcut. + + Projectors are used to project an invidividual Population's or a + collective GroupPopulation's on to other populations. + + The currently available cases are projecting: + - from an invidivual to a group + - from a group to an individual + - from a group to an individual with a unique role + + For example, if there are two entities, person (Entity) and household + (GroupEntity), on which calculations can be run (Population and + GroupPopulation respectively), and there is a Variable "rent" defined for + the household entity, then `person.household("rent")` will assign a rent to + every person within that household. + + Behind the scenes, this is done thanks to a Projector, and this function is + used to find the appropriate one for each case. In the above example, the + `shortcut` argument would be "household", and the `population` argument + whould be the Population linked to the "person" Entity in the context + of a specific Simulation and TaxBenefitSystem. + + Args: + population (Population | GroupPopulation): Where to project from. + shortcut (str): Where to project to. + parent: ??? Examples: >>> from openfisca_core import ( From b6b3a34d0503e0f134e9f8c3b2c0c2e0779e3d74 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 5 Oct 2023 18:32:14 +0200 Subject: [PATCH 043/188] Bump version --- CHANGELOG.md | 10 ++++++++++ openfisca_core/entities/helpers.py | 4 ++-- openfisca_core/projectors/helpers.py | 5 +++-- openfisca_core/projectors/typing.py | 29 +--------------------------- setup.py | 2 +- 5 files changed, 17 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc4aef54bf..2ed7811618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 41.4.0 [#1197](https://github.com/openfisca/openfisca-core/pull/1197) + +#### New features + +- Add `entities.find_role()` to find roles by key and `max`. + +#### Technical changes + +- Document `projectors.get_projector_from_shortcut()`. + ## 41.3.0 [#1200](https://github.com/openfisca/openfisca-core/pull/1200) > As `TracingParameterNodeAtInstant` is a wrapper for `ParameterNodeAtInstant` diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 91131c3903..f3d988536a 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -4,7 +4,7 @@ from .entity import Entity from .group_entity import GroupEntity -from .role import Role +from .typing import Role def build_entity( @@ -28,7 +28,7 @@ def build_entity( def find_role( roles: Iterable[Role], key: str, *, total: int | None = None ) -> Role | None: - """Find a role in an entity. + """Find a Role in a GrupEntity. Args: group_entity (GroupEntity): The entity to search in. diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index 4269ad6387..196801b712 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -1,10 +1,11 @@ from __future__ import annotations from collections.abc import Mapping +from openfisca_core.entities.typing import Entity, GroupEntity, Role from openfisca_core import entities, projectors -from .typing import Entity, GroupEntity, GroupPopulation, Population +from .typing import GroupPopulation, Population def projectable(function): @@ -124,7 +125,7 @@ def get_projector_from_shortcut( return projectors.FirstPersonToEntityProjector(population, parent) if isinstance(entity, entities.GroupEntity): - role: entities.Role | None = entities.find_role(entity.roles, shortcut, total=1) + role: Role | None = entities.find_role(entity.roles, shortcut, total=1) if role is not None: return projectors.UniqueRoleToEntityProjector(population, role, parent) diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py index 5093f28e96..7b9170ad71 100644 --- a/openfisca_core/projectors/typing.py +++ b/openfisca_core/projectors/typing.py @@ -1,37 +1,10 @@ from __future__ import annotations from collections.abc import Mapping +from openfisca_core.entities.typing import Entity, GroupEntity, Role from typing import Iterable, Protocol -class Entity(Protocol): - @property - def is_person(self) -> bool: - ... - - -class GroupEntity(Protocol): - @property - def containing_entities(self) -> Iterable[str]: - ... - - @property - def is_person(self) -> bool: - ... - - @property - def flattened_roles(self) -> Iterable[Role]: - ... - - @property - def roles(self) -> Iterable[Role]: - ... - - -class Role(Protocol): - ... - - class Population(Protocol): @property def entity(self) -> Entity: diff --git a/setup.py b/setup.py index 1722223da2..2b25a81e1f 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.3.0", + version="41.4.0", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From e1858ea077b89c89cf892120d76cf41cf0a79c3f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 7 Oct 2023 12:48:28 +0200 Subject: [PATCH 044/188] Fix linter --- openfisca_core/projectors/typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py index 7b9170ad71..5dbbd90e07 100644 --- a/openfisca_core/projectors/typing.py +++ b/openfisca_core/projectors/typing.py @@ -1,8 +1,8 @@ from __future__ import annotations from collections.abc import Mapping -from openfisca_core.entities.typing import Entity, GroupEntity, Role -from typing import Iterable, Protocol +from openfisca_core.entities.typing import Entity, GroupEntity +from typing import Protocol class Population(Protocol): From 5ad1278ef0f830c05d6e2e773eab73fc0c0269c8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga-Alvarado Date: Thu, 30 Nov 2023 14:24:24 +0000 Subject: [PATCH 045/188] refactor: change martian for animal in example Co-authored-by: Mahdi Ben Jelloul --- openfisca_core/projectors/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index 196801b712..b99ac2884f 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -63,7 +63,7 @@ def get_projector_from_shortcut( >>> roles = [ ... {"key": "person", "max": 1}, - ... {"key": "martian", "subroles": ["cat", "dog"]}, + ... {"key": "animal", "subroles": ["cat", "dog"]}, ... ] >>> group_entity_2 = entities.GroupEntity("household", "", "", "", roles) From 1fc064ed886d1ae3677d8ccd1063b6ef9d7687a6 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga-Alvarado Date: Thu, 30 Nov 2023 14:26:18 +0000 Subject: [PATCH 046/188] doc: fix typo in entities documentation Co-authored-by: Mahdi Ben Jelloul --- openfisca_core/entities/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index f3d988536a..ca2e0b33a7 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -28,7 +28,7 @@ def build_entity( def find_role( roles: Iterable[Role], key: str, *, total: int | None = None ) -> Role | None: - """Find a Role in a GrupEntity. + """Find a Role in a GroupEntity. Args: group_entity (GroupEntity): The entity to search in. From d54135dca691b80d856908a2812b7594c618dae3 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Sun, 12 Nov 2023 12:31:25 +0100 Subject: [PATCH 047/188] Avoid confusing error when using axes and missing entities --- .../simulations/simulation_builder.py | 8 +++---- tests/core/test_axes.py | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index d48ac0152e..107d40ce20 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -288,12 +288,12 @@ def add_default_group_entity(self, persons_ids, entity): persons_count = len(persons_ids) self.entity_ids[entity.plural] = persons_ids self.entity_counts[entity.plural] = persons_count - self.memberships[entity.plural] = numpy.arange( + self.memberships[entity.plural] = list(numpy.arange( 0, persons_count, dtype=numpy.int32 - ) - self.roles[entity.plural] = numpy.repeat( + )) + self.roles[entity.plural] = list(numpy.repeat( entity.flattened_roles[0], persons_count - ) + )) def add_group_entity(self, persons_plural, persons_ids, entity, instances_json): """ diff --git a/tests/core/test_axes.py b/tests/core/test_axes.py index eb0f58caac..e734e0476d 100644 --- a/tests/core/test_axes.py +++ b/tests/core/test_axes.py @@ -350,3 +350,27 @@ def test_simulation_with_axes(tax_benefit_system): [0, 0, 0, 0, 0, 0] ) assert simulation.get_array("rent", "2018-11") == pytest.approx([0, 0, 3000, 0]) + +# Test for missing group entities with build_from_entities() + +def test_simulation_with_axes_missing_entities(tax_benefit_system): + input_yaml = """ + persons: + Alicia: {salary: {2018-11: 0}} + Javier: {} + Tom: {} + axes: + - + - count: 2 + name: rent + min: 0 + max: 3000 + period: 2018-11 + """ + data = test_runner.yaml.safe_load(input_yaml) + simulation = SimulationBuilder().build_from_entities(tax_benefit_system, data) + assert simulation.get_array("salary", "2018-11") == pytest.approx( + [0, 0, 0, 0, 0, 0] + ) + # Since a household is synthesized for each person, we have six: + assert simulation.get_array("rent", "2018-11") == pytest.approx([0, 0, 0, 3000, 0, 0]) From 0d277b2fabc24e5e4ee0ce7a671c824dcc679fa4 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 30 Nov 2023 16:18:38 +0100 Subject: [PATCH 048/188] refactor: apply linter --- .../simulations/simulation_builder.py | 42 +++++++++---------- tests/core/test_axes.py | 6 ++- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 107d40ce20..4761672d26 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -1,4 +1,4 @@ -from typing import Dict, Iterable, List +from collections.abc import Iterable import copy @@ -21,26 +21,26 @@ def __init__(self): ) # JSON input - Memory of known input values. Indexed by variable or axis name. - self.input_buffer: Dict[ - variables.Variable.name, Dict[str(periods.period), numpy.array] + self.input_buffer: dict[ + variables.Variable.name, dict[str(periods.period), numpy.array] ] = {} - self.populations: Dict[entities.Entity.key, populations.Population] = {} + self.populations: dict[entities.Entity.key, populations.Population] = {} # JSON input - Number of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_ids``, including axes. - self.entity_counts: Dict[entities.Entity.plural, int] = {} + self.entity_counts: dict[entities.Entity.plural, int] = {} # JSON input - List of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_counts``. - self.entity_ids: Dict[entities.Entity.plural, List[int]] = {} + self.entity_ids: dict[entities.Entity.plural, list[int]] = {} # Links entities with persons. For each person index in persons ids list, set entity index in entity ids id. E.g.: self.memberships[entity.plural][person_index] = entity_ids.index(instance_id) - self.memberships: Dict[entities.Entity.plural, List[int]] = {} - self.roles: Dict[entities.Entity.plural, List[int]] = {} + self.memberships: dict[entities.Entity.plural, list[int]] = {} + self.roles: dict[entities.Entity.plural, list[int]] = {} - self.variable_entities: Dict[variables.Variable.name, entities.Entity] = {} + self.variable_entities: dict[variables.Variable.name, entities.Entity] = {} self.axes = [[]] - self.axes_entity_counts: Dict[entities.Entity.plural, int] = {} - self.axes_entity_ids: Dict[entities.Entity.plural, List[int]] = {} - self.axes_memberships: Dict[entities.Entity.plural, List[int]] = {} - self.axes_roles: Dict[entities.Entity.plural, List[int]] = {} + self.axes_entity_counts: dict[entities.Entity.plural, int] = {} + self.axes_entity_ids: dict[entities.Entity.plural, list[int]] = {} + self.axes_memberships: dict[entities.Entity.plural, list[int]] = {} + self.axes_roles: dict[entities.Entity.plural, list[int]] = {} def build_from_dict(self, tax_benefit_system, input_dict): """ @@ -288,12 +288,12 @@ def add_default_group_entity(self, persons_ids, entity): persons_count = len(persons_ids) self.entity_ids[entity.plural] = persons_ids self.entity_counts[entity.plural] = persons_count - self.memberships[entity.plural] = list(numpy.arange( - 0, persons_count, dtype=numpy.int32 - )) - self.roles[entity.plural] = list(numpy.repeat( - entity.flattened_roles[0], persons_count - )) + self.memberships[entity.plural] = list( + numpy.arange(0, persons_count, dtype=numpy.int32) + ) + self.roles[entity.plural] = list( + numpy.repeat(entity.flattened_roles[0], persons_count) + ) def add_group_entity(self, persons_plural, persons_ids, entity, instances_json): """ @@ -513,7 +513,7 @@ def raise_period_mismatch(self, entity, json, e): # We do a basic research to find the culprit path culprit_path = next( dpath.util.search( - json, "*/{}/{}".format(e.variable_name, str(e.period)), yielded=True + json, f"*/{e.variable_name}/{str(e.period)}", yielded=True ), None, ) @@ -620,7 +620,7 @@ def expand_axes(self): # Set input self.input_buffer[axis_name][str(axis_period)] = array else: - first_axes_count: List[int] = ( + first_axes_count: list[int] = ( parallel_axes[0]["count"] for parallel_axes in self.axes ) axes_linspaces = [ diff --git a/tests/core/test_axes.py b/tests/core/test_axes.py index e734e0476d..19ccb8cf8c 100644 --- a/tests/core/test_axes.py +++ b/tests/core/test_axes.py @@ -351,8 +351,10 @@ def test_simulation_with_axes(tax_benefit_system): ) assert simulation.get_array("rent", "2018-11") == pytest.approx([0, 0, 3000, 0]) + # Test for missing group entities with build_from_entities() + def test_simulation_with_axes_missing_entities(tax_benefit_system): input_yaml = """ persons: @@ -373,4 +375,6 @@ def test_simulation_with_axes_missing_entities(tax_benefit_system): [0, 0, 0, 0, 0, 0] ) # Since a household is synthesized for each person, we have six: - assert simulation.get_array("rent", "2018-11") == pytest.approx([0, 0, 0, 3000, 0, 0]) + assert simulation.get_array("rent", "2018-11") == pytest.approx( + [0, 0, 0, 3000, 0, 0] + ) From e223ea25f02526263e69620177885da1c367e306 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 4 Dec 2023 16:31:17 +0100 Subject: [PATCH 049/188] refactor: add axis class --- openfisca_core/simulations/_axis.py | 38 +++++++++++++ .../simulations/simulation_builder.py | 54 ++++++++++--------- openfisca_core/simulations/typing.py | 12 +++++ 3 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 openfisca_core/simulations/_axis.py create mode 100644 openfisca_core/simulations/typing.py diff --git a/openfisca_core/simulations/_axis.py b/openfisca_core/simulations/_axis.py new file mode 100644 index 0000000000..03db6c40b8 --- /dev/null +++ b/openfisca_core/simulations/_axis.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import dataclasses + + +@dataclasses.dataclass(frozen=True) +class _Axis: + """Base data class for axes (no domain logic). + + Examples: + >>> axis = _Axis(name = "salary", count = 3, min = 0, max = 3000) + + >>> axis + _Axis(name='salary', count=3, min=0, max=3000, period=None, index=0) + + >>> axis.name + 'salary' + + """ + + #: The name of the Variable whose values are to be expanded. + name: str + + #: The Number of "steps" to take when expanding Variable (between `min` and + # `max`, we create a line and split it in `count` number of parts). + count: int + + #: The starting numerical value for the Axis expansion. + min: float + + #: The up-to numerical value for the Axis expansion. + max: float + + #: The period at which the expansion will take place over. + period: str | int | None = None + + #: Axis position relative to other equidistant axes. + index: int = 0 diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 4761672d26..fada6bc9f3 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -8,7 +8,9 @@ from openfisca_core import entities, errors, periods, populations, variables from . import helpers +from ._axis import _Axis from .simulation import Simulation +from .typing import AxisParams class SimulationBuilder: @@ -128,7 +130,13 @@ def build_from_entities(self, tax_benefit_system, input_dict): self.add_default_group_entity(persons_ids, entity_class) if axes: - self.axes = axes + for axis in axes[0]: + self.add_parallel_axis(axis) + + if len(axes) >= 1: + for axis in axes[1:]: + self.add_perpendicular_axis(axis[0]) + self.expand_axes() try: @@ -546,14 +554,14 @@ def get_roles(self, entity_name): # Return empty array for the "persons" entity return self.axes_roles.get(entity_name, self.roles.get(entity_name, [])) - def add_parallel_axis(self, axis): + def add_parallel_axis(self, axis: AxisParams) -> None: # All parallel axes have the same count and entity. # Search for a compatible axis, if none exists, error out - self.axes[0].append(axis) + self.axes[0].append(_Axis(**axis)) - def add_perpendicular_axis(self, axis): + def add_perpendicular_axis(self, axis: AxisParams) -> None: # This adds an axis perpendicular to all previous dimensions - self.axes.append([axis]) + self.axes.append([_Axis(**axis)]) def expand_axes(self): # This method should be idempotent & allow change in axes @@ -562,7 +570,7 @@ def expand_axes(self): cell_count = 1 for parallel_axes in perpendicular_dimensions: first_axis = parallel_axes[0] - axis_count = first_axis["count"] + axis_count = first_axis.count cell_count *= axis_count # Scale the "prototype" situation, repeating it cell_count times @@ -598,14 +606,14 @@ def expand_axes(self): if len(self.axes) == 1 and len(self.axes[0]): parallel_axes = self.axes[0] first_axis = parallel_axes[0] - axis_count: int = first_axis["count"] - axis_entity = self.get_variable_entity(first_axis["name"]) + axis_count: int = first_axis.count + axis_entity = self.get_variable_entity(first_axis.name) axis_entity_step_size = self.entity_counts[axis_entity.plural] # Distribute values along axes for axis in parallel_axes: - axis_index = axis.get("index", 0) - axis_period = axis.get("period", self.default_period) - axis_name = axis["name"] + axis_index = axis.index + axis_period = axis.period or self.default_period + axis_name = axis.name variable = axis_entity.get_variable(axis_name) array = self.get_input(axis_name, str(axis_period)) if array is None: @@ -613,15 +621,15 @@ def expand_axes(self): elif array.size == axis_entity_step_size: array = numpy.tile(array, axis_count) array[axis_index::axis_entity_step_size] = numpy.linspace( - axis["min"], - axis["max"], + axis.min, + axis.max, num=axis_count, ) # Set input self.input_buffer[axis_name][str(axis_period)] = array else: first_axes_count: list[int] = ( - parallel_axes[0]["count"] for parallel_axes in self.axes + parallel_axes[0].count for parallel_axes in self.axes ) axes_linspaces = [ numpy.linspace(0, axis_count - 1, num=axis_count) @@ -630,14 +638,14 @@ def expand_axes(self): axes_meshes = numpy.meshgrid(*axes_linspaces) for parallel_axes, mesh in zip(self.axes, axes_meshes): first_axis = parallel_axes[0] - axis_count = first_axis["count"] - axis_entity = self.get_variable_entity(first_axis["name"]) + axis_count = first_axis.count + axis_entity = self.get_variable_entity(first_axis.name) axis_entity_step_size = self.entity_counts[axis_entity.plural] # Distribute values along the grid for axis in parallel_axes: - axis_index = axis.get("index", 0) - axis_period = axis["period"] or self.default_period - axis_name = axis["name"] + axis_index = axis.index + axis_period = axis.period or self.default_period + axis_name = axis.name variable = axis_entity.get_variable(axis_name) array = self.get_input(axis_name, str(axis_period)) if array is None: @@ -646,11 +654,9 @@ def expand_axes(self): ) elif array.size == axis_entity_step_size: array = numpy.tile(array, cell_count) - array[axis_index::axis_entity_step_size] = axis[ - "min" - ] + mesh.reshape(cell_count) * (axis["max"] - axis["min"]) / ( - axis_count - 1 - ) + array[axis_index::axis_entity_step_size] = axis.min + mesh.reshape( + cell_count + ) * (axis.max - axis.min) / (axis_count - 1) self.input_buffer[axis_name][str(axis_period)] = array def get_variable_entity(self, variable_name: str): diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py new file mode 100644 index 0000000000..42debeff36 --- /dev/null +++ b/openfisca_core/simulations/typing.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from typing import TypedDict + + +class AxisParams(TypedDict, total=False): + name: str + count: int + min: float + max: float + period: str | int + index: int From 6590266b99e51de78c67469da8e1968a93b404e8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 4 Dec 2023 17:59:09 +0100 Subject: [PATCH 050/188] test: fix types 1 --- .../simulations/simulation_builder.py | 68 +++++++++++-------- openfisca_core/simulations/typing.py | 27 +++++++- openfisca_core/variables/variable.py | 3 +- 3 files changed, 66 insertions(+), 32 deletions(-) diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index fada6bc9f3..236e2068d0 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -1,16 +1,19 @@ -from collections.abc import Iterable +from __future__ import annotations + +from collections.abc import Iterable, Sequence +from numpy.typing import NDArray as Array import copy import dpath.util import numpy -from openfisca_core import entities, errors, periods, populations, variables +from openfisca_core import errors, periods from . import helpers from ._axis import _Axis from .simulation import Simulation -from .typing import AxisParams +from .typing import AxisParams, Entity, Population, Role class SimulationBuilder: @@ -23,26 +26,24 @@ def __init__(self): ) # JSON input - Memory of known input values. Indexed by variable or axis name. - self.input_buffer: dict[ - variables.Variable.name, dict[str(periods.period), numpy.array] - ] = {} - self.populations: dict[entities.Entity.key, populations.Population] = {} + self.input_buffer: dict[str, dict[str, Array]] = {} + self.populations: dict[str, Population] = {} # JSON input - Number of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_ids``, including axes. - self.entity_counts: dict[entities.Entity.plural, int] = {} + self.entity_counts: dict[str, int] = {} # JSON input - List of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_counts``. - self.entity_ids: dict[entities.Entity.plural, list[int]] = {} + self.entity_ids: dict[str, list[int]] = {} # Links entities with persons. For each person index in persons ids list, set entity index in entity ids id. E.g.: self.memberships[entity.plural][person_index] = entity_ids.index(instance_id) - self.memberships: dict[entities.Entity.plural, list[int]] = {} - self.roles: dict[entities.Entity.plural, list[int]] = {} + self.memberships: dict[str, list[int]] = {} + self.roles: dict[str, list[int]] = {} - self.variable_entities: dict[variables.Variable.name, entities.Entity] = {} + self.variable_entities: dict[str, Entity] = {} self.axes = [[]] - self.axes_entity_counts: dict[entities.Entity.plural, int] = {} - self.axes_entity_ids: dict[entities.Entity.plural, list[int]] = {} - self.axes_memberships: dict[entities.Entity.plural, list[int]] = {} - self.axes_roles: dict[entities.Entity.plural, list[int]] = {} + self.axes_entity_counts: dict[str, int] = {} + self.axes_entity_ids: dict[str, list[str]] = {} + self.axes_memberships: dict[str, list[int]] = {} + self.axes_roles: dict[str, list[int]] = {} def build_from_dict(self, tax_benefit_system, input_dict): """ @@ -395,9 +396,10 @@ def set_default_period(self, period_str): if period_str: self.default_period = str(periods.period(period_str)) - def get_input(self, variable, period_str): + def get_input(self, variable: str, period_str: str) -> Array | None: if variable not in self.input_buffer: self.input_buffer[variable] = {} + return self.input_buffer[variable].get(period_str) def check_persons_to_allocate( @@ -535,11 +537,11 @@ def raise_period_mismatch(self, entity, json, e): raise errors.SituationParsingError(path, e.message) # Returns the total number of instances of this entity, including when there is replication along axes - def get_count(self, entity_name): + def get_count(self, entity_name: str) -> int: return self.axes_entity_counts.get(entity_name, self.entity_counts[entity_name]) # Returns the ids of instances of this entity, including when there is replication along axes - def get_ids(self, entity_name): + def get_ids(self, entity_name: str) -> list[str]: return self.axes_entity_ids.get(entity_name, self.entity_ids[entity_name]) # Returns the memberships of individuals in this entity, including when there is replication along axes @@ -550,7 +552,7 @@ def get_memberships(self, entity_name): ) # Returns the roles of individuals in this entity, including when there is replication along axes - def get_roles(self, entity_name): + def get_roles(self, entity_name: str) -> Sequence[Role]: # Return empty array for the "persons" entity return self.axes_roles.get(entity_name, self.roles.get(entity_name, [])) @@ -563,14 +565,14 @@ def add_perpendicular_axis(self, axis: AxisParams) -> None: # This adds an axis perpendicular to all previous dimensions self.axes.append([_Axis(**axis)]) - def expand_axes(self): + def expand_axes(self) -> None: # This method should be idempotent & allow change in axes - perpendicular_dimensions = self.axes + perpendicular_dimensions: list[list[_Axis]] = self.axes + cell_count: int = 1 - cell_count = 1 for parallel_axes in perpendicular_dimensions: - first_axis = parallel_axes[0] - axis_count = first_axis.count + first_axis: _Axis = parallel_axes[0] + axis_count: int = first_axis.count cell_count *= axis_count # Scale the "prototype" situation, repeating it cell_count times @@ -580,10 +582,16 @@ def expand_axes(self): self.get_count(entity_name) * cell_count ) # Adjust ids - original_ids = self.get_ids(entity_name) * cell_count - indices = numpy.arange(0, cell_count * self.entity_counts[entity_name]) - adjusted_ids = [id + str(ix) for id, ix in zip(original_ids, indices)] + original_ids: list[str] = self.get_ids(entity_name) * cell_count + indices: Array[numpy.int_] = numpy.arange( + 0, cell_count * self.entity_counts[entity_name] + ) + adjusted_ids: list[str] = [ + original_id + str(index) + for original_id, index in zip(original_ids, indices) + ] self.axes_entity_ids[entity_name] = adjusted_ids + # Adjust roles original_roles = self.get_roles(entity_name) adjusted_roles = original_roles * cell_count @@ -659,8 +667,8 @@ def expand_axes(self): ) * (axis.max - axis.min) / (axis_count - 1) self.input_buffer[axis_name][str(axis_period)] = array - def get_variable_entity(self, variable_name: str): + def get_variable_entity(self, variable_name: str) -> Entity: return self.variable_entities[variable_name] - def register_variable(self, variable_name: str, entity): + def register_variable(self, variable_name: str, entity: Entity) -> None: self.variable_entities[variable_name] = entity diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py index 42debeff36..7bd35e713a 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/typing.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TypedDict +from numpy.typing import NDArray as Array +from typing import Protocol, TypedDict class AxisParams(TypedDict, total=False): @@ -10,3 +11,27 @@ class AxisParams(TypedDict, total=False): max: float period: str | int index: int + + +class Entity(Protocol): + plural: str | None + + def get_variable( + self, + variable_name: str, + check_existence: bool = False, + ) -> Variable | None: + ... + + +class Population(Protocol): + ... + + +class Role(Protocol): + ... + + +class Variable(Protocol): + def default_array(self, array_size: int) -> Array: + ... diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 2693a31211..c3118b55d2 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -1,5 +1,6 @@ from __future__ import annotations +from numpy.typing import NDArray as Array from openfisca_core.types import Formula, Instant from typing import Optional, Union @@ -467,7 +468,7 @@ def check_set_value(self, value): return value - def default_array(self, array_size): + def default_array(self, array_size: int) -> Array: array = numpy.empty(array_size, dtype=self.dtype) if self.value_type == Enum: array.fill(self.default_value.index) From cf1b176cd51aa5fbd4724c40be8b75626b393348 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 7 Dec 2023 23:59:02 +0100 Subject: [PATCH 051/188] refactor: document attrs --- .../simulations/simulation_builder.py | 66 +++++++++++++------ openfisca_core/simulations/typing.py | 38 +++++++++-- 2 files changed, 81 insertions(+), 23 deletions(-) diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 236e2068d0..ecb7e15e09 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -13,38 +13,66 @@ from . import helpers from ._axis import _Axis from .simulation import Simulation -from .typing import AxisParams, Entity, Population, Role +from .typing import AxisParams, Entity, GroupPopulation, Role class SimulationBuilder: - def __init__(self): - self.default_period = ( - None # Simulation period used for variables when no period is defined - ) - self.persons_plural = ( - None # Plural name for person entity in current tax and benefits system - ) + #: Simulation period used for variables when no period is defined + default_period: str | None = None - # JSON input - Memory of known input values. Indexed by variable or axis name. - self.input_buffer: dict[str, dict[str, Array]] = {} - self.populations: dict[str, Population] = {} - # JSON input - Number of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_ids``, including axes. - self.entity_counts: dict[str, int] = {} - # JSON input - List of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_counts``. - self.entity_ids: dict[str, list[int]] = {} + #: Plural name for person entity in current tax and benefits system + persons_plural: str | None = None + + #: JSON input - Memory of known input values. Indexed by variable or + #: axis name. + input_buffer: dict[str, dict[str, Array]] = {} + + #: ? + populations: dict[str, GroupPopulation] = {} + + #: JSON input - Number of items of each entity type. Indexed by entities + # plural names. Should be consistent with ``entity_ids``, including axes. + entity_counts: dict[str, int] + + #: JSON input - List of items of each entity type. Indexed by entities + #: plural names. Should be consistent with ``entity_counts``. + entity_ids: dict[str, list[str]] = {} + + #: Links entities with persons. For each person index in persons ids list, + #: set entity index in entity ids id. + memberships: dict[str, list[int]] = {} - # Links entities with persons. For each person index in persons ids list, set entity index in entity ids id. E.g.: self.memberships[entity.plural][person_index] = entity_ids.index(instance_id) - self.memberships: dict[str, list[int]] = {} - self.roles: dict[str, list[int]] = {} + #: ? + roles: dict[str, list[Role]] = {} - self.variable_entities: dict[str, Entity] = {} + #: ? + variable_entities: dict[str, Entity] = {} + #: Axes use for axes expansion. + axes: list[list[_Axis]] + + #: ? + axes_entity_counts: dict[str, int] + + #: ? + axes_entity_ids: dict[str, list[str]] + + #: ? + axes_memberships: dict[str, list[int]] + + #: ? + axes_roles: dict[str, list[Role]] + + def __init__(self): + self.input_buffer: dict[str, dict[str, Array]] = {} + self.entity_counts: dict[str, int] = {} self.axes = [[]] self.axes_entity_counts: dict[str, int] = {} self.axes_entity_ids: dict[str, list[str]] = {} self.axes_memberships: dict[str, list[int]] = {} self.axes_roles: dict[str, list[int]] = {} + def build_from_dict(self, tax_benefit_system, input_dict): """ Build a simulation from ``input_dict`` diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py index 7bd35e713a..571a7ad060 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/typing.py @@ -1,7 +1,8 @@ from __future__ import annotations from numpy.typing import NDArray as Array -from typing import Protocol, TypedDict +from typing import Any, Protocol, TypedDict +from collections.abc import Sequence class AxisParams(TypedDict, total=False): @@ -19,19 +20,48 @@ class Entity(Protocol): def get_variable( self, variable_name: str, - check_existence: bool = False, + check_existence: bool = ..., ) -> Variable | None: ... -class Population(Protocol): +class Holder(Protocol): + variable: Variable + def set_input( + self, + period: Period, + array: Array[Any] | Sequence[Any], + ) -> Array[Any] | None: + ... + + +class Period(Protocol): ... +class Population(Protocol): + count: int + entity: Entity + + def get_holder(self, variable_name: str) -> Holder: + ... + + class Role(Protocol): ... +class GroupPopulation(Population): + def nb_persons(self, role: Role | None = ...) -> int: + ... + + +class TaxBenefitSystem(Protocol): + ... + + class Variable(Protocol): - def default_array(self, array_size: int) -> Array: + end: str + + def default_array(self, array_size: int) -> Array[Any]: ... From 237ac2caa79071d0b747ea325cd16495dedd763c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 8 Dec 2023 04:00:10 +0100 Subject: [PATCH 052/188] test: fix types 2 --- openfisca_core/simulations/helpers.py | 12 +- openfisca_core/simulations/simulation.py | 15 +- .../simulations/simulation_builder.py | 144 +++++++++++++----- openfisca_core/simulations/typing.py | 77 +++++++++- tests/fixtures/entities.py | 8 +- 5 files changed, 203 insertions(+), 53 deletions(-) diff --git a/openfisca_core/simulations/helpers.py b/openfisca_core/simulations/helpers.py index b559f7d071..18d0838485 100644 --- a/openfisca_core/simulations/helpers.py +++ b/openfisca_core/simulations/helpers.py @@ -1,5 +1,11 @@ +from __future__ import annotations + +from typing import Type, Union + from openfisca_core.errors import SituationParsingError +from .typing import FullyDefinedParamsWithoutShortcut + def calculate_output_add(simulation, variable_name: str, period): return simulation.calculate_add(variable_name, period) @@ -9,7 +15,11 @@ def calculate_output_divide(simulation, variable_name: str, period): return simulation.calculate_divide(variable_name, period) -def check_type(input, input_type, path=None): +def check_type( + input: FullyDefinedParamsWithoutShortcut, + input_type: Type[Union[dict, list, str]], + path: list[str] | None = None, +) -> None: json_type_map = { dict: "Object", list: "Array", diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 304d7338ab..fbd9bea536 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,7 +1,6 @@ from __future__ import annotations -from openfisca_core.types import Population, TaxBenefitSystem, Variable -from typing import Dict, NamedTuple, Optional, Set +from typing import Dict, NamedTuple, Optional, Set, Union import tempfile import warnings @@ -11,6 +10,8 @@ from openfisca_core import commons, errors, indexed_enums, periods, tracers from openfisca_core import warnings as core_warnings +from .typing import GroupPopulation, SinglePopulation, TaxBenefitSystem, Variable + class Simulation: """ @@ -18,13 +19,13 @@ class Simulation: """ tax_benefit_system: TaxBenefitSystem - populations: Dict[str, Population] + populations: Dict[str, Union[SinglePopulation, GroupPopulation]] invalidated_caches: Set[Cache] def __init__( self, tax_benefit_system: TaxBenefitSystem, - populations: Dict[str, Population], + populations: Dict[str, Union[SinglePopulation, GroupPopulation]], ): """ This constructor is reserved for internal use; see :any:`SimulationBuilder`, @@ -530,7 +531,7 @@ def set_input(self, variable_name: str, period, value): return self.get_holder(variable_name).set_input(period, value) - def get_variable_population(self, variable_name: str) -> Population: + def get_variable_population(self, variable_name: str) -> GroupPopulation: variable: Optional[Variable] variable = self.tax_benefit_system.get_variable( @@ -542,7 +543,7 @@ def get_variable_population(self, variable_name: str) -> Population: return self.populations[variable.entity.key] - def get_population(self, plural: Optional[str] = None) -> Optional[Population]: + def get_population(self, plural: Optional[str] = None) -> Optional[GroupPopulation]: return next( ( population @@ -555,7 +556,7 @@ def get_population(self, plural: Optional[str] = None) -> Optional[Population]: def get_entity( self, plural: Optional[str] = None, - ) -> Optional[Population]: + ) -> Optional[GroupPopulation]: population = self.get_population(plural) return population and population.entity diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index ecb7e15e09..cbe4e8c691 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -1,7 +1,9 @@ from __future__ import annotations +import typing from collections.abc import Iterable, Sequence from numpy.typing import NDArray as Array +from typing import Any, Optional import copy @@ -13,7 +15,22 @@ from . import helpers from ._axis import _Axis from .simulation import Simulation -from .typing import AxisParams, Entity, GroupPopulation, Role +from .typing import ( + AxesParams, + AxisParams, + Entity, + FullyDefinedParams, + FullyDefinedParamsWithoutAxes, + FullyDefinedParamsWithoutShortcut, + GroupEntity, + GroupEntityParams, + GroupPopulation, + Role, + SingleEntity, + SingleEntityParams, + TaxBenefitSystem, + VariableParams, +) class SimulationBuilder: @@ -25,7 +42,7 @@ class SimulationBuilder: #: JSON input - Memory of known input values. Indexed by variable or #: axis name. - input_buffer: dict[str, dict[str, Array]] = {} + input_buffer: dict[str, dict[str, Array[Any]]] = {} #: ? populations: dict[str, GroupPopulation] = {} @@ -63,17 +80,20 @@ class SimulationBuilder: #: ? axes_roles: dict[str, list[Role]] - def __init__(self): - self.input_buffer: dict[str, dict[str, Array]] = {} - self.entity_counts: dict[str, int] = {} + def __init__(self) -> None: + self.input_buffer = {} + self.entity_counts = {} self.axes = [[]] - self.axes_entity_counts: dict[str, int] = {} - self.axes_entity_ids: dict[str, list[str]] = {} - self.axes_memberships: dict[str, list[int]] = {} - self.axes_roles: dict[str, list[int]] = {} + self.axes_entity_counts = {} + self.axes_entity_ids = {} + self.axes_memberships = {} + self.axes_roles = {} - - def build_from_dict(self, tax_benefit_system, input_dict): + def build_from_dict( + self, + tax_benefit_system: TaxBenefitSystem, + input_dict: FullyDefinedParams | VariableParams, + ) -> Simulation: """ Build a simulation from ``input_dict`` @@ -83,26 +103,36 @@ def build_from_dict(self, tax_benefit_system, input_dict): :return: A :any:`Simulation` """ - input_dict = self.explicit_singular_entities(tax_benefit_system, input_dict) if any( key in tax_benefit_system.entities_plural() for key in input_dict.keys() ): - return self.build_from_entities(tax_benefit_system, input_dict) + fully_defined_params = typing.cast(FullyDefinedParams, input_dict) + fully_defined_params_without_shortcut = self.explicit_singular_entities( + tax_benefit_system, fully_defined_params + ) + return self.build_from_entities( + tax_benefit_system, fully_defined_params_without_shortcut + ) else: - return self.build_from_variables(tax_benefit_system, input_dict) + variable_params = typing.cast(VariableParams, input_dict) + return self.build_from_variables(tax_benefit_system, variable_params) - def build_from_entities(self, tax_benefit_system, input_dict): + def build_from_entities( + self, + tax_benefit_system: TaxBenefitSystem, + input_dict: FullyDefinedParamsWithoutShortcut, + ) -> Simulation: """ Build a simulation from a Python dict ``input_dict`` fully specifying entities. Examples: - >>> simulation_builder.build_from_entities({ - 'persons': {'Javier': { 'salary': {'2018-11': 2000}}}, - 'households': {'household': {'parents': ['Javier']}} - }) + >>> { + ... 'persons': {'Javier': { 'salary': {'2018-11': 2000}}}, + ... 'households': {'household': {'parents': ['Javier']}} + ... } """ - input_dict = copy.deepcopy(input_dict) + fully_defined_params = copy.deepcopy(input_dict) simulation = Simulation( tax_benefit_system, tax_benefit_system.instantiate_entities() @@ -114,12 +144,20 @@ def build_from_entities(self, tax_benefit_system, input_dict): variable_name, simulation.get_variable_population(variable_name).entity ) - helpers.check_type(input_dict, dict, ["error"]) - axes = input_dict.pop("axes", None) + helpers.check_type(fully_defined_params, dict, ["error"]) + axes = typing.cast(Optional[AxesParams], fully_defined_params.get("axes", None)) + full_defined_params_without_axes = typing.cast( + FullyDefinedParamsWithoutAxes, + { + key: value + for key, value in fully_defined_params.items() + if key != "axes" + }, + ) unexpected_entities = [ entity - for entity in input_dict + for entity in full_defined_params_without_axes if entity not in tax_benefit_system.entities_plural() ] if unexpected_entities: @@ -137,20 +175,32 @@ def build_from_entities(self, tax_benefit_system, input_dict): ", ".join(tax_benefit_system.entities_plural()), ), ) - persons_json = input_dict.get(tax_benefit_system.person_entity.plural, None) + + person_entity: SingleEntity = tax_benefit_system.person_entity + + if person_entity.plural is None: + raise ValueError("#TODO") + + persons_json = typing.cast( + Optional[SingleEntityParams], + full_defined_params_without_axes.get(person_entity.plural, None), + ) if not persons_json: raise errors.SituationParsingError( - [tax_benefit_system.person_entity.plural], + [person_entity.plural], "No {0} found. At least one {0} must be defined to run a simulation.".format( - tax_benefit_system.person_entity.key + person_entity.key ), ) persons_ids = self.add_person_entity(simulation.persons.entity, persons_json) for entity_class in tax_benefit_system.group_entities: - instances_json = input_dict.get(entity_class.plural) + instances_json: GroupEntityParams | None = ( + full_defined_params_without_axes.get(entity_class.plural) + ) + if instances_json is not None: self.add_group_entity( self.persons_plural, persons_ids, entity_class, instances_json @@ -182,17 +232,17 @@ def build_from_entities(self, tax_benefit_system, input_dict): return simulation - def build_from_variables(self, tax_benefit_system, input_dict): + def build_from_variables( + self, tax_benefit_system: TaxBenefitSystem, input_dict: VariableParams + ) -> Simulation: """ Build a simulation from a Python dict ``input_dict`` describing variables values without expliciting entities. This method uses :any:`build_default_simulation` to infer an entity structure Example: + >>> {'salary': {'2016-10': 12000}} - >>> simulation_builder.build_from_variables( - {'salary': {'2016-10': 12000}} - ) """ count = helpers._get_person_count(input_dict) simulation = self.build_default_simulation(tax_benefit_system, count) @@ -275,7 +325,9 @@ def join_with_persons( def build(self, tax_benefit_system): return Simulation(tax_benefit_system, self.populations) - def explicit_singular_entities(self, tax_benefit_system, input_dict): + def explicit_singular_entities( + self, tax_benefit_system: TaxBenefitSystem, input_dict: FullyDefinedParams + ) -> FullyDefinedParamsWithoutShortcut: """ Preprocess ``input_dict`` to explicit entities defined using the single-entity shortcut @@ -309,6 +361,9 @@ def add_person_entity(self, entity, instances_json): """ Add the simulation's instances of the persons entity as described in ``instances_json``. """ + if entity.plural is None: + raise ValueError("#TODO") + helpers.check_type(instances_json, dict, [entity.plural]) entity_ids = list(map(str, instances_json.keys())) self.persons_plural = entity.plural @@ -321,21 +376,34 @@ def add_person_entity(self, entity, instances_json): return self.get_ids(entity.plural) - def add_default_group_entity(self, persons_ids, entity): + def add_default_group_entity( + self, persons_ids: list[str], entity: GroupEntity + ) -> None: + if entity.plural is None: + raise ValueError("#TODO") + persons_count = len(persons_ids) + roles = list(entity.flattened_roles) self.entity_ids[entity.plural] = persons_ids self.entity_counts[entity.plural] = persons_count self.memberships[entity.plural] = list( numpy.arange(0, persons_count, dtype=numpy.int32) ) - self.roles[entity.plural] = list( - numpy.repeat(entity.flattened_roles[0], persons_count) - ) + self.roles[entity.plural] = [roles[0]] * persons_count - def add_group_entity(self, persons_plural, persons_ids, entity, instances_json): + def add_group_entity( + self, + persons_plural: str, + persons_ids: list[str], + entity: GroupEntity, + instances_json, + ) -> None: """ Add all instances of one of the model's entities as described in ``instances_json``. """ + if entity.plural is None: + raise ValueError("#TODO") + helpers.check_type(instances_json, dict, [entity.plural]) entity_ids = list(map(str, instances_json.keys())) @@ -682,7 +750,7 @@ def expand_axes(self) -> None: axis_index = axis.index axis_period = axis.period or self.default_period axis_name = axis.name - variable = axis_entity.get_variable(axis_name) + variable = axis_entity.get_variable(axis_name, check_existence=True) array = self.get_input(axis_name, str(axis_period)) if array is None: array = variable.default_array( diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py index 571a7ad060..c7ae301b3f 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/typing.py @@ -1,8 +1,35 @@ from __future__ import annotations -from numpy.typing import NDArray as Array -from typing import Any, Protocol, TypedDict +import typing from collections.abc import Sequence +from numpy.typing import NDArray as Array +from typing import Any, Iterable, Protocol, TypedDict, Union +from typing_extensions import TypeAlias + +import numpy + +VariableParams: TypeAlias = dict[str, dict[str, object]] + +SingleEntityParams: TypeAlias = dict[str, VariableParams] + +GroupEntityShortcutParams: TypeAlias = dict[str, Union[str, list[str]]] + +GroupEntityParams: TypeAlias = dict[str, GroupEntityShortcutParams] + +AxesParams: TypeAlias = list[list["AxisParams"]] + +FullyDefinedParamsWithoutAxes: TypeAlias = dict[ + str, Union[SingleEntityParams, GroupEntityParams] +] + +FullyDefinedParamsWithoutShortcut: TypeAlias = dict[ + str, Union[SingleEntityParams, GroupEntityParams, AxesParams] +] + +FullyDefinedParams: TypeAlias = dict[ + str, + Union[SingleEntityParams, GroupEntityParams, GroupEntityShortcutParams, AxesParams], +] class AxisParams(TypedDict, total=False): @@ -15,8 +42,25 @@ class AxisParams(TypedDict, total=False): class Entity(Protocol): + key: str plural: str | None + @typing.overload + def get_variable( + self, + variable_name: str, + check_existence: bool = True, + ) -> Variable: + ... + + @typing.overload + def get_variable( + self, + variable_name: str, + check_existence: bool = False, + ) -> Variable | None: + ... + def get_variable( self, variable_name: str, @@ -25,12 +69,21 @@ def get_variable( ... +class SingleEntity(Entity): + ... + + +class GroupEntity(Entity): + flattened_roles: Iterable[Role] + + class Holder(Protocol): variable: Variable + def set_input( self, period: Period, - array: Array[Any] | Sequence[Any], + array: Array[Any] | Sequence[object], ) -> Array[Any] | None: ... @@ -39,15 +92,20 @@ class Period(Protocol): ... +class Role(Protocol): + ... + + class Population(Protocol): count: int entity: Entity + ids: Array[numpy.str_] def get_holder(self, variable_name: str) -> Holder: ... -class Role(Protocol): +class SinglePopulation(Population): ... @@ -57,7 +115,16 @@ def nb_persons(self, role: Role | None = ...) -> int: class TaxBenefitSystem(Protocol): - ... + person_entity: SingleEntity + variables: dict[str, Variable] + + def entities_plural(self) -> Iterable[str]: + ... + + def instantiate_entities( + self, + ) -> dict[str, Union[SinglePopulation, GroupPopulation]]: + ... class Variable(Protocol): diff --git a/tests/fixtures/entities.py b/tests/fixtures/entities.py index 6cab008f43..4d103f10d3 100644 --- a/tests/fixtures/entities.py +++ b/tests/fixtures/entities.py @@ -6,7 +6,9 @@ class TestEntity(Entity): - def get_variable(self, variable_name: str): + def get_variable( + self, variable_name: str, check_existence: bool = False + ) -> TestVariable: result = TestVariable(self) result.name = variable_name return result @@ -16,7 +18,9 @@ def check_variable_defined_for_entity(self, variable_name: str): class TestGroupEntity(GroupEntity): - def get_variable(self, variable_name: str): + def get_variable( + self, variable_name: str, check_existence: bool = False + ) -> TestVariable: result = TestVariable(self) result.name = variable_name return result From 9bb296fe8e99c40a72b20e413b1611a6e773787a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 8 Dec 2023 08:01:47 +0100 Subject: [PATCH 053/188] test: fix types 3 --- openfisca_core/simulations/_type_guards.py | 22 +++++ openfisca_core/simulations/helpers.py | 4 +- .../simulations/simulation_builder.py | 85 +++++++++---------- openfisca_core/simulations/typing.py | 26 +++--- 4 files changed, 74 insertions(+), 63 deletions(-) create mode 100644 openfisca_core/simulations/_type_guards.py diff --git a/openfisca_core/simulations/_type_guards.py b/openfisca_core/simulations/_type_guards.py new file mode 100644 index 0000000000..b68e36d8e2 --- /dev/null +++ b/openfisca_core/simulations/_type_guards.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import Iterable +from typing_extensions import TypeGuard + +from .typing import AbbrParams, AxesParams, ExplParams, ImplParams, Params + + +def is_abbr_spec(params: Params, items: Iterable[str]) -> TypeGuard[AbbrParams]: + return any(key in items for key in params.keys()) or params == {} + + +def is_impl_spec(params: Params, items: Iterable[str]) -> TypeGuard[ImplParams]: + return set(params).intersection(items) + + +def is_expl_spec(params: Params, items: Iterable[str]) -> TypeGuard[ExplParams]: + return any(key in items for key in params.keys()) + + +def is_axes_spec(params: ExplParams | Params) -> TypeGuard[AxesParams]: + return params.get("axes", None) is not None diff --git a/openfisca_core/simulations/helpers.py b/openfisca_core/simulations/helpers.py index 18d0838485..77d229a5db 100644 --- a/openfisca_core/simulations/helpers.py +++ b/openfisca_core/simulations/helpers.py @@ -4,7 +4,7 @@ from openfisca_core.errors import SituationParsingError -from .typing import FullyDefinedParamsWithoutShortcut +from .typing import ExplParams def calculate_output_add(simulation, variable_name: str, period): @@ -16,7 +16,7 @@ def calculate_output_divide(simulation, variable_name: str, period): def check_type( - input: FullyDefinedParamsWithoutShortcut, + input: ExplParams, input_type: Type[Union[dict, list, str]], path: list[str] | None = None, ) -> None: diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index cbe4e8c691..cc652b43db 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -3,7 +3,7 @@ import typing from collections.abc import Iterable, Sequence from numpy.typing import NDArray as Array -from typing import Any, Optional +from typing import Any import copy @@ -14,22 +14,21 @@ from . import helpers from ._axis import _Axis +from ._type_guards import is_abbr_spec, is_axes_spec, is_expl_spec, is_impl_spec from .simulation import Simulation from .typing import ( - AxesParams, + AbbrParams, AxisParams, Entity, - FullyDefinedParams, - FullyDefinedParamsWithoutAxes, - FullyDefinedParamsWithoutShortcut, + ExplParams, GroupEntity, - GroupEntityParams, GroupPopulation, + ImplParams, + NoAxParams, + Params, Role, SingleEntity, - SingleEntityParams, TaxBenefitSystem, - VariableParams, ) @@ -92,7 +91,7 @@ def __init__(self) -> None: def build_from_dict( self, tax_benefit_system: TaxBenefitSystem, - input_dict: FullyDefinedParams | VariableParams, + input_dict: Params, ) -> Simulation: """ Build a simulation from ``input_dict`` @@ -103,24 +102,27 @@ def build_from_dict( :return: A :any:`Simulation` """ - if any( - key in tax_benefit_system.entities_plural() for key in input_dict.keys() + if is_impl_spec( + params := input_dict, tax_benefit_system.entities_by_singular() ): - fully_defined_params = typing.cast(FullyDefinedParams, input_dict) - fully_defined_params_without_shortcut = self.explicit_singular_entities( - tax_benefit_system, fully_defined_params - ) - return self.build_from_entities( - tax_benefit_system, fully_defined_params_without_shortcut - ) - else: - variable_params = typing.cast(VariableParams, input_dict) - return self.build_from_variables(tax_benefit_system, variable_params) + expl = self.explicit_singular_entities(tax_benefit_system, params) + return self.build_from_entities(tax_benefit_system, expl) + + if is_expl_spec(input_dict, tax_benefit_system.entities_plural()): + return self.build_from_entities(tax_benefit_system, input_dict) + + if is_axes_spec(input_dict): + raise ValueError("#TODO") + + if is_abbr_spec(params := input_dict, tax_benefit_system.variables.keys()): + return self.build_from_variables(tax_benefit_system, params) + + raise ValueError("#TODO") def build_from_entities( self, tax_benefit_system: TaxBenefitSystem, - input_dict: FullyDefinedParamsWithoutShortcut, + input_dict: ExplParams, ) -> Simulation: """ Build a simulation from a Python dict ``input_dict`` fully specifying entities. @@ -132,7 +134,7 @@ def build_from_entities( ... 'households': {'household': {'parents': ['Javier']}} ... } """ - fully_defined_params = copy.deepcopy(input_dict) + input_dict = copy.deepcopy(input_dict) simulation = Simulation( tax_benefit_system, tax_benefit_system.instantiate_entities() @@ -144,20 +146,18 @@ def build_from_entities( variable_name, simulation.get_variable_population(variable_name).entity ) - helpers.check_type(fully_defined_params, dict, ["error"]) - axes = typing.cast(Optional[AxesParams], fully_defined_params.get("axes", None)) - full_defined_params_without_axes = typing.cast( - FullyDefinedParamsWithoutAxes, - { - key: value - for key, value in fully_defined_params.items() - if key != "axes" - }, - ) + helpers.check_type(input_dict, dict, ["error"]) + + axes: list[list[AxisParams]] | None = None + + if is_axes_spec(input_dict): + axes = input_dict.pop("axes") + + params: NoAxParams = typing.cast(NoAxParams, input_dict) unexpected_entities = [ entity - for entity in full_defined_params_without_axes + for entity in params if entity not in tax_benefit_system.entities_plural() ] if unexpected_entities: @@ -181,10 +181,7 @@ def build_from_entities( if person_entity.plural is None: raise ValueError("#TODO") - persons_json = typing.cast( - Optional[SingleEntityParams], - full_defined_params_without_axes.get(person_entity.plural, None), - ) + persons_json = params.get(person_entity.plural, None) if not persons_json: raise errors.SituationParsingError( @@ -197,9 +194,7 @@ def build_from_entities( persons_ids = self.add_person_entity(simulation.persons.entity, persons_json) for entity_class in tax_benefit_system.group_entities: - instances_json: GroupEntityParams | None = ( - full_defined_params_without_axes.get(entity_class.plural) - ) + instances_json = params.get(entity_class.plural) if instances_json is not None: self.add_group_entity( @@ -233,7 +228,7 @@ def build_from_entities( return simulation def build_from_variables( - self, tax_benefit_system: TaxBenefitSystem, input_dict: VariableParams + self, tax_benefit_system: TaxBenefitSystem, input_dict: AbbrParams ) -> Simulation: """ Build a simulation from a Python dict ``input_dict`` describing variables values without expliciting entities. @@ -326,8 +321,8 @@ def build(self, tax_benefit_system): return Simulation(tax_benefit_system, self.populations) def explicit_singular_entities( - self, tax_benefit_system: TaxBenefitSystem, input_dict: FullyDefinedParams - ) -> FullyDefinedParamsWithoutShortcut: + self, tax_benefit_system: TaxBenefitSystem, input_dict: ImplParams + ) -> ExplParams: """ Preprocess ``input_dict`` to explicit entities defined using the single-entity shortcut @@ -342,8 +337,6 @@ def explicit_singular_entities( singular_keys = set(input_dict).intersection( tax_benefit_system.entities_by_singular() ) - if not singular_keys: - return input_dict result = { entity_id: entity_description diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py index c7ae301b3f..a9aae33c36 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/typing.py @@ -8,28 +8,21 @@ import numpy -VariableParams: TypeAlias = dict[str, dict[str, object]] +AbbrParams: TypeAlias = dict[str, dict[str, object]] -SingleEntityParams: TypeAlias = dict[str, VariableParams] +FullParams: TypeAlias = dict[str, dict[str, AbbrParams]] -GroupEntityShortcutParams: TypeAlias = dict[str, Union[str, list[str]]] +AxesParams: TypeAlias = dict[str, list[list["AxisParams"]]] -GroupEntityParams: TypeAlias = dict[str, GroupEntityShortcutParams] +EntsParams: TypeAlias = dict[str, Union[str, list[str]]] -AxesParams: TypeAlias = list[list["AxisParams"]] +ImplParams: TypeAlias = Union[EntsParams, FullParams, AxesParams] -FullyDefinedParamsWithoutAxes: TypeAlias = dict[ - str, Union[SingleEntityParams, GroupEntityParams] -] +ExplParams: TypeAlias = Union[dict[str, EntsParams], FullParams, AxesParams] -FullyDefinedParamsWithoutShortcut: TypeAlias = dict[ - str, Union[SingleEntityParams, GroupEntityParams, AxesParams] -] +NoAxParams: TypeAlias = Union[dict[str, EntsParams], FullParams] -FullyDefinedParams: TypeAlias = dict[ - str, - Union[SingleEntityParams, GroupEntityParams, GroupEntityShortcutParams, AxesParams], -] +Params: TypeAlias = Union[dict[str, EntsParams], FullParams, AxesParams, AbbrParams] class AxisParams(TypedDict, total=False): @@ -118,6 +111,9 @@ class TaxBenefitSystem(Protocol): person_entity: SingleEntity variables: dict[str, Variable] + def entities_by_singular(self) -> Iterable[str]: + ... + def entities_plural(self) -> Iterable[str]: ... From 82ddcc172e1e839070380e65f87d2e05256647b3 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 8 Dec 2023 08:47:13 +0100 Subject: [PATCH 054/188] feat: fail with axes and missing specs --- .../simulations/simulation_builder.py | 46 +++++++++---------- tests/core/test_axes.py | 15 +++--- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index cc652b43db..600bd02fa3 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -102,22 +102,21 @@ def build_from_dict( :return: A :any:`Simulation` """ - if is_impl_spec( - params := input_dict, tax_benefit_system.entities_by_singular() - ): - expl = self.explicit_singular_entities(tax_benefit_system, params) - return self.build_from_entities(tax_benefit_system, expl) + variables = tax_benefit_system.variables.keys() - if is_expl_spec(input_dict, tax_benefit_system.entities_plural()): - return self.build_from_entities(tax_benefit_system, input_dict) + singular = tax_benefit_system.entities_by_singular() - if is_axes_spec(input_dict): - raise ValueError("#TODO") + plural = tax_benefit_system.entities_plural() - if is_abbr_spec(params := input_dict, tax_benefit_system.variables.keys()): - return self.build_from_variables(tax_benefit_system, params) + if is_impl_spec(input_dict, singular): + expl = self.explicit_singular_entities(tax_benefit_system, input_dict) + return self.build_from_entities(tax_benefit_system, expl) - raise ValueError("#TODO") + if is_expl_spec(input_dict, plural): + return self.build_from_entities(tax_benefit_system, input_dict) + + if is_abbr_spec(input_dict, variables): + return self.build_from_variables(tax_benefit_system, input_dict) def build_from_entities( self, @@ -178,9 +177,6 @@ def build_from_entities( person_entity: SingleEntity = tax_benefit_system.person_entity - if person_entity.plural is None: - raise ValueError("#TODO") - persons_json = params.get(person_entity.plural, None) if not persons_json: @@ -200,6 +196,17 @@ def build_from_entities( self.add_group_entity( self.persons_plural, persons_ids, entity_class, instances_json ) + + elif axes: + raise errors.SituationParsingError( + [entity_class.plural], + f"We could not find any specified {entity_class.plural}. " + "In order to expand over axes, all group entities and roles " + "must be fully specified. For further support, please do " + "not hesitate to take a look at the official documentation: " + "https://openfisca.org/doc/simulate/replicate-simulation-inputs.html.", + ) + else: self.add_default_group_entity(persons_ids, entity_class) @@ -354,9 +361,6 @@ def add_person_entity(self, entity, instances_json): """ Add the simulation's instances of the persons entity as described in ``instances_json``. """ - if entity.plural is None: - raise ValueError("#TODO") - helpers.check_type(instances_json, dict, [entity.plural]) entity_ids = list(map(str, instances_json.keys())) self.persons_plural = entity.plural @@ -372,9 +376,6 @@ def add_person_entity(self, entity, instances_json): def add_default_group_entity( self, persons_ids: list[str], entity: GroupEntity ) -> None: - if entity.plural is None: - raise ValueError("#TODO") - persons_count = len(persons_ids) roles = list(entity.flattened_roles) self.entity_ids[entity.plural] = persons_ids @@ -394,9 +395,6 @@ def add_group_entity( """ Add all instances of one of the model's entities as described in ``instances_json``. """ - if entity.plural is None: - raise ValueError("#TODO") - helpers.check_type(instances_json, dict, [entity.plural]) entity_ids = list(map(str, instances_json.keys())) diff --git a/tests/core/test_axes.py b/tests/core/test_axes.py index 19ccb8cf8c..799439e9c4 100644 --- a/tests/core/test_axes.py +++ b/tests/core/test_axes.py @@ -1,5 +1,6 @@ import pytest +from openfisca_core import errors from openfisca_core.simulations import SimulationBuilder from openfisca_core.tools import test_runner @@ -322,7 +323,7 @@ def test_add_perpendicular_axis_on_an_existing_variable_with_input(persons): ) -# Integration test +# Integration tests def test_simulation_with_axes(tax_benefit_system): @@ -370,11 +371,7 @@ def test_simulation_with_axes_missing_entities(tax_benefit_system): period: 2018-11 """ data = test_runner.yaml.safe_load(input_yaml) - simulation = SimulationBuilder().build_from_entities(tax_benefit_system, data) - assert simulation.get_array("salary", "2018-11") == pytest.approx( - [0, 0, 0, 0, 0, 0] - ) - # Since a household is synthesized for each person, we have six: - assert simulation.get_array("rent", "2018-11") == pytest.approx( - [0, 0, 0, 3000, 0, 0] - ) + with pytest.raises(errors.SituationParsingError) as error: + SimulationBuilder().build_from_dict(tax_benefit_system, data) + assert "In order to expand over axes" in error.value() + assert "all group entities and roles must be fully specified" in error.value() From 2491ba14651174b4eb7373562d6d73da4493d929 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 8 Dec 2023 08:51:51 +0100 Subject: [PATCH 055/188] Bump version --- CHANGELOG.md | 6 + openfisca_core/simulations/_axis.py | 38 ------ openfisca_core/simulations/helpers.py | 12 +- openfisca_core/simulations/simulation.py | 15 +-- .../simulations/simulation_builder.py | 123 +++++++----------- openfisca_core/variables/variable.py | 3 +- setup.py | 2 +- tests/core/test_tracers.py | 8 +- 8 files changed, 71 insertions(+), 136 deletions(-) delete mode 100644 openfisca_core/simulations/_axis.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed7811618..99c4419bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.4.1 [#1202](https://github.com/openfisca/openfisca-core/pull/1202) + +#### Technical changes + +- Check that entities are fully specified when expanding over axes. + ## 41.4.0 [#1197](https://github.com/openfisca/openfisca-core/pull/1197) #### New features diff --git a/openfisca_core/simulations/_axis.py b/openfisca_core/simulations/_axis.py deleted file mode 100644 index 03db6c40b8..0000000000 --- a/openfisca_core/simulations/_axis.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -import dataclasses - - -@dataclasses.dataclass(frozen=True) -class _Axis: - """Base data class for axes (no domain logic). - - Examples: - >>> axis = _Axis(name = "salary", count = 3, min = 0, max = 3000) - - >>> axis - _Axis(name='salary', count=3, min=0, max=3000, period=None, index=0) - - >>> axis.name - 'salary' - - """ - - #: The name of the Variable whose values are to be expanded. - name: str - - #: The Number of "steps" to take when expanding Variable (between `min` and - # `max`, we create a line and split it in `count` number of parts). - count: int - - #: The starting numerical value for the Axis expansion. - min: float - - #: The up-to numerical value for the Axis expansion. - max: float - - #: The period at which the expansion will take place over. - period: str | int | None = None - - #: Axis position relative to other equidistant axes. - index: int = 0 diff --git a/openfisca_core/simulations/helpers.py b/openfisca_core/simulations/helpers.py index 77d229a5db..b559f7d071 100644 --- a/openfisca_core/simulations/helpers.py +++ b/openfisca_core/simulations/helpers.py @@ -1,11 +1,5 @@ -from __future__ import annotations - -from typing import Type, Union - from openfisca_core.errors import SituationParsingError -from .typing import ExplParams - def calculate_output_add(simulation, variable_name: str, period): return simulation.calculate_add(variable_name, period) @@ -15,11 +9,7 @@ def calculate_output_divide(simulation, variable_name: str, period): return simulation.calculate_divide(variable_name, period) -def check_type( - input: ExplParams, - input_type: Type[Union[dict, list, str]], - path: list[str] | None = None, -) -> None: +def check_type(input, input_type, path=None): json_type_map = { dict: "Object", list: "Array", diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index fbd9bea536..304d7338ab 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Dict, NamedTuple, Optional, Set, Union +from openfisca_core.types import Population, TaxBenefitSystem, Variable +from typing import Dict, NamedTuple, Optional, Set import tempfile import warnings @@ -10,8 +11,6 @@ from openfisca_core import commons, errors, indexed_enums, periods, tracers from openfisca_core import warnings as core_warnings -from .typing import GroupPopulation, SinglePopulation, TaxBenefitSystem, Variable - class Simulation: """ @@ -19,13 +18,13 @@ class Simulation: """ tax_benefit_system: TaxBenefitSystem - populations: Dict[str, Union[SinglePopulation, GroupPopulation]] + populations: Dict[str, Population] invalidated_caches: Set[Cache] def __init__( self, tax_benefit_system: TaxBenefitSystem, - populations: Dict[str, Union[SinglePopulation, GroupPopulation]], + populations: Dict[str, Population], ): """ This constructor is reserved for internal use; see :any:`SimulationBuilder`, @@ -531,7 +530,7 @@ def set_input(self, variable_name: str, period, value): return self.get_holder(variable_name).set_input(period, value) - def get_variable_population(self, variable_name: str) -> GroupPopulation: + def get_variable_population(self, variable_name: str) -> Population: variable: Optional[Variable] variable = self.tax_benefit_system.get_variable( @@ -543,7 +542,7 @@ def get_variable_population(self, variable_name: str) -> GroupPopulation: return self.populations[variable.entity.key] - def get_population(self, plural: Optional[str] = None) -> Optional[GroupPopulation]: + def get_population(self, plural: Optional[str] = None) -> Optional[Population]: return next( ( population @@ -556,7 +555,7 @@ def get_population(self, plural: Optional[str] = None) -> Optional[GroupPopulati def get_entity( self, plural: Optional[str] = None, - ) -> Optional[GroupPopulation]: + ) -> Optional[Population]: population = self.get_population(plural) return population and population.entity diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 600bd02fa3..365bf9db05 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -3,17 +3,16 @@ import typing from collections.abc import Iterable, Sequence from numpy.typing import NDArray as Array -from typing import Any +from typing import Dict, List import copy import dpath.util import numpy -from openfisca_core import errors, periods +from openfisca_core import entities, errors, periods, populations, variables from . import helpers -from ._axis import _Axis from ._type_guards import is_abbr_spec, is_axes_spec, is_expl_spec, is_impl_spec from .simulation import Simulation from .typing import ( @@ -22,7 +21,6 @@ Entity, ExplParams, GroupEntity, - GroupPopulation, ImplParams, NoAxParams, Params, @@ -33,60 +31,35 @@ class SimulationBuilder: - #: Simulation period used for variables when no period is defined - default_period: str | None = None - - #: Plural name for person entity in current tax and benefits system - persons_plural: str | None = None - - #: JSON input - Memory of known input values. Indexed by variable or - #: axis name. - input_buffer: dict[str, dict[str, Array[Any]]] = {} - - #: ? - populations: dict[str, GroupPopulation] = {} - - #: JSON input - Number of items of each entity type. Indexed by entities - # plural names. Should be consistent with ``entity_ids``, including axes. - entity_counts: dict[str, int] - - #: JSON input - List of items of each entity type. Indexed by entities - #: plural names. Should be consistent with ``entity_counts``. - entity_ids: dict[str, list[str]] = {} - - #: Links entities with persons. For each person index in persons ids list, - #: set entity index in entity ids id. - memberships: dict[str, list[int]] = {} - - #: ? - roles: dict[str, list[Role]] = {} - - #: ? - variable_entities: dict[str, Entity] = {} - - #: Axes use for axes expansion. - axes: list[list[_Axis]] - - #: ? - axes_entity_counts: dict[str, int] + def __init__(self): + self.default_period = ( + None # Simulation period used for variables when no period is defined + ) + self.persons_plural = ( + None # Plural name for person entity in current tax and benefits system + ) - #: ? - axes_entity_ids: dict[str, list[str]] + # JSON input - Memory of known input values. Indexed by variable or axis name. + self.input_buffer: Dict[ + variables.Variable.name, Dict[str(periods.period), numpy.array] + ] = {} + self.populations: Dict[entities.Entity.key, populations.Population] = {} + # JSON input - Number of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_ids``, including axes. + self.entity_counts: Dict[entities.Entity.plural, int] = {} + # JSON input - List of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_counts``. + self.entity_ids: Dict[entities.Entity.plural, List[int]] = {} - #: ? - axes_memberships: dict[str, list[int]] + # Links entities with persons. For each person index in persons ids list, set entity index in entity ids id. E.g.: self.memberships[entity.plural][person_index] = entity_ids.index(instance_id) + self.memberships: Dict[entities.Entity.plural, List[int]] = {} + self.roles: Dict[entities.Entity.plural, List[int]] = {} - #: ? - axes_roles: dict[str, list[Role]] + self.variable_entities: Dict[variables.Variable.name, entities.Entity] = {} - def __init__(self) -> None: - self.input_buffer = {} - self.entity_counts = {} self.axes = [[]] - self.axes_entity_counts = {} - self.axes_entity_ids = {} - self.axes_memberships = {} - self.axes_roles = {} + self.axes_entity_counts: Dict[entities.Entity.plural, int] = {} + self.axes_entity_ids: Dict[entities.Entity.plural, List[int]] = {} + self.axes_memberships: Dict[entities.Entity.plural, List[int]] = {} + self.axes_roles: Dict[entities.Entity.plural, List[int]] = {} def build_from_dict( self, @@ -646,20 +619,20 @@ def get_roles(self, entity_name: str) -> Sequence[Role]: def add_parallel_axis(self, axis: AxisParams) -> None: # All parallel axes have the same count and entity. # Search for a compatible axis, if none exists, error out - self.axes[0].append(_Axis(**axis)) + self.axes[0].append(axis) def add_perpendicular_axis(self, axis: AxisParams) -> None: # This adds an axis perpendicular to all previous dimensions - self.axes.append([_Axis(**axis)]) + self.axes.append([axis]) def expand_axes(self) -> None: # This method should be idempotent & allow change in axes - perpendicular_dimensions: list[list[_Axis]] = self.axes + perpendicular_dimensions: list[list[AxisParams]] = self.axes cell_count: int = 1 for parallel_axes in perpendicular_dimensions: - first_axis: _Axis = parallel_axes[0] - axis_count: int = first_axis.count + first_axis: AxisParams = parallel_axes[0] + axis_count: int = first_axis["count"] cell_count *= axis_count # Scale the "prototype" situation, repeating it cell_count times @@ -701,14 +674,14 @@ def expand_axes(self) -> None: if len(self.axes) == 1 and len(self.axes[0]): parallel_axes = self.axes[0] first_axis = parallel_axes[0] - axis_count: int = first_axis.count - axis_entity = self.get_variable_entity(first_axis.name) + axis_count: int = first_axis["count"] + axis_entity = self.get_variable_entity(first_axis["name"]) axis_entity_step_size = self.entity_counts[axis_entity.plural] # Distribute values along axes for axis in parallel_axes: - axis_index = axis.index - axis_period = axis.period or self.default_period - axis_name = axis.name + axis_index = axis.get("index", 0) + axis_period = axis.get("period", self.default_period) + axis_name = axis["name"] variable = axis_entity.get_variable(axis_name) array = self.get_input(axis_name, str(axis_period)) if array is None: @@ -716,15 +689,15 @@ def expand_axes(self) -> None: elif array.size == axis_entity_step_size: array = numpy.tile(array, axis_count) array[axis_index::axis_entity_step_size] = numpy.linspace( - axis.min, - axis.max, + axis["min"], + axis["max"], num=axis_count, ) # Set input self.input_buffer[axis_name][str(axis_period)] = array else: first_axes_count: list[int] = ( - parallel_axes[0].count for parallel_axes in self.axes + parallel_axes[0]["count"] for parallel_axes in self.axes ) axes_linspaces = [ numpy.linspace(0, axis_count - 1, num=axis_count) @@ -733,14 +706,14 @@ def expand_axes(self) -> None: axes_meshes = numpy.meshgrid(*axes_linspaces) for parallel_axes, mesh in zip(self.axes, axes_meshes): first_axis = parallel_axes[0] - axis_count = first_axis.count - axis_entity = self.get_variable_entity(first_axis.name) + axis_count = first_axis["count"] + axis_entity = self.get_variable_entity(first_axis["name"]) axis_entity_step_size = self.entity_counts[axis_entity.plural] # Distribute values along the grid for axis in parallel_axes: - axis_index = axis.index - axis_period = axis.period or self.default_period - axis_name = axis.name + axis_index = axis.get("index", 0) + axis_period = axis.get("period", self.default_period) + axis_name = axis["name"] variable = axis_entity.get_variable(axis_name, check_existence=True) array = self.get_input(axis_name, str(axis_period)) if array is None: @@ -749,9 +722,11 @@ def expand_axes(self) -> None: ) elif array.size == axis_entity_step_size: array = numpy.tile(array, cell_count) - array[axis_index::axis_entity_step_size] = axis.min + mesh.reshape( - cell_count - ) * (axis.max - axis.min) / (axis_count - 1) + array[axis_index::axis_entity_step_size] = axis[ + "min" + ] + mesh.reshape(cell_count) * (axis["max"] - axis["min"]) / ( + axis_count - 1 + ) self.input_buffer[axis_name][str(axis_period)] = array def get_variable_entity(self, variable_name: str) -> Entity: diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index c3118b55d2..2693a31211 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -1,6 +1,5 @@ from __future__ import annotations -from numpy.typing import NDArray as Array from openfisca_core.types import Formula, Instant from typing import Optional, Union @@ -468,7 +467,7 @@ def check_set_value(self, value): return value - def default_array(self, array_size: int) -> Array: + def default_array(self, array_size): array = numpy.empty(array_size, dtype=self.dtype) if self.value_type == Enum: array.fill(self.default_value.index) diff --git a/setup.py b/setup.py index 2b25a81e1f..fc0bbfef75 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.4.0", + version="41.4.1", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index 1ecfd09aa6..d0e25f8667 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -20,6 +20,10 @@ from .parameters_fancy_indexing.test_fancy_indexing import parameters +class TestException(Exception): + ... + + class StubSimulation(Simulation): def __init__(self): self.exception = None @@ -91,9 +95,9 @@ def test_tracer_contract(tracer): def test_exception_robustness(): simulation = StubSimulation() simulation.tracer = MockTracer() - simulation.exception = Exception(":-o") + simulation.exception = TestException(":-o") - with raises(Exception): + with raises(TestException): simulation.calculate("a", 2017) assert simulation.tracer.calculation_start_recorded From 698386cd63e52c261fb5a4c721f863beb933c206 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Dec 2023 00:53:29 +0100 Subject: [PATCH 056/188] refactor: extract build default simulation --- .../simulations/_build_default_simulation.py | 179 ++++++++++++++++ openfisca_core/simulations/typing.py | 202 ++++++++++++------ 2 files changed, 310 insertions(+), 71 deletions(-) create mode 100644 openfisca_core/simulations/_build_default_simulation.py diff --git a/openfisca_core/simulations/_build_default_simulation.py b/openfisca_core/simulations/_build_default_simulation.py new file mode 100644 index 0000000000..7ce4009ed1 --- /dev/null +++ b/openfisca_core/simulations/_build_default_simulation.py @@ -0,0 +1,179 @@ +"""This module contains the _BuildDefaultSimulation class.""" + +from typing import Union +from typing_extensions import Self + +import numpy + +from .simulation import Simulation +from .typing import Entity, Population, TaxBenefitSystem + + +class _BuildDefaultSimulation: + """Build a default simulation. + + Args: + tax_benefit_system(TaxBenefitSystem): The tax-benefit system. + count(int): The number of periods. + + Examples: + >>> from openfisca_core.entities import Entity as SingleEntity + >>> from openfisca_core.entities import GroupEntity + >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + + >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} + >>> single_entity = SingleEntity("dog", "dogs", "", "") + >>> group_entity = GroupEntity("pack", "packs", "", "", [role]) + >>> entities = {single_entity, group_entity} + >>> tax_benefit_system = TaxBenefitSystem(entities) + >>> count = 1 + >>> builder = ( + ... _BuildDefaultSimulation(tax_benefit_system, count) + ... .add_count() + ... .add_ids() + ... .add_members_entity_id() + ... ) + + >>> builder.count + 1 + + >>> sorted(builder.populations.keys()) + ['dog', 'pack'] + + >>> sorted(builder.simulation.populations.keys()) + ['dog', 'pack'] + + >>> sorted(builder.tax_benefit_system.entities_by_singular().keys()) + ['dog', 'pack'] + + """ + + #: The number of Population. + count: int + + #: The built populations. + populations: dict[str, Union[Population[Entity]]] + + #: The built simulation. + simulation: Simulation + + #: The tax-benefit system. + tax_benefit_system: TaxBenefitSystem + + def __init__( + self, tax_benefit_system: TaxBenefitSystem, count: int + ) -> None: + self.count = count + self.tax_benefit_system = tax_benefit_system + self.populations = tax_benefit_system.instantiate_entities() + self.simulation = Simulation(tax_benefit_system, self.populations) + + def add_count(self) -> Self: + """Add the number of Population to the simulation. + + Returns: + _BuildDefaultSimulation: The builder. + + Examples: + >>> from openfisca_core.entities import Entity as SingleEntity + >>> from openfisca_core.entities import GroupEntity + >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + + >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} + >>> single_entity = SingleEntity("dog", "dogs", "", "") + >>> group_entity = GroupEntity("pack", "packs", "", "", [role]) + >>> entities = {single_entity, group_entity} + >>> tax_benefit_system = TaxBenefitSystem(entities) + >>> count = 2 + >>> builder = _BuildDefaultSimulation(tax_benefit_system, count) + + >>> builder.add_count() + <..._BuildDefaultSimulation object at ...> + + >>> builder.populations["dog"].count + 2 + + >>> builder.populations["pack"].count + 2 + + """ + + for population in self.populations.values(): + population.count = self.count + + return self + + def add_ids(self) -> Self: + """Add the populations ids to the simulation. + + Returns: + _BuildDefaultSimulation: The builder. + + Examples: + >>> from openfisca_core.entities import Entity as SingleEntity + >>> from openfisca_core.entities import GroupEntity + >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + + >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} + >>> single_entity = SingleEntity("dog", "dogs", "", "") + >>> group_entity = GroupEntity("pack", "packs", "", "", [role]) + >>> entities = {single_entity, group_entity} + >>> tax_benefit_system = TaxBenefitSystem(entities) + >>> count = 2 + >>> builder = _BuildDefaultSimulation(tax_benefit_system, count) + + >>> builder.add_ids() + <..._BuildDefaultSimulation object at ...> + + >>> builder.populations["dog"].ids + array([0, 1]) + + >>> builder.populations["pack"].ids + array([0, 1]) + + """ + + for population in self.populations.values(): + population.ids = numpy.array(range(self.count)) + + return self + + def add_members_entity_id(self) -> Self: + """Add ??? + + Each SingleEntity has its own GroupEntity. + + Returns: + _BuildDefaultSimulation: The builder. + + Examples: + >>> from openfisca_core.entities import Entity as SingleEntity + >>> from openfisca_core.entities import GroupEntity + >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + + >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} + >>> single_entity = SingleEntity("dog", "dogs", "", "") + >>> group_entity = GroupEntity("pack", "packs", "", "", [role]) + >>> entities = {single_entity, group_entity} + >>> tax_benefit_system = TaxBenefitSystem(entities) + >>> count = 2 + >>> builder = _BuildDefaultSimulation(tax_benefit_system, count) + + >>> builder.add_members_entity_id() + <..._BuildDefaultSimulation object at ...> + + >>> population = builder.populations["pack"] + + >>> hasattr(population, "members_entity_id") + True + + >>> population.members_entity_id + array([0, 1]) + + """ + + for population in self.populations.values(): + if hasattr(population, "members_entity_id"): + population.members_entity_id = numpy.array(range(self.count)) + + return self diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py index a9aae33c36..6bf31ec1ac 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/typing.py @@ -1,130 +1,190 @@ +"""Type aliases of OpenFisca models to use in the context of simulations.""" + from __future__ import annotations -import typing +from abc import abstractmethod from collections.abc import Sequence from numpy.typing import NDArray as Array -from typing import Any, Iterable, Protocol, TypedDict, Union -from typing_extensions import TypeAlias +from typing import Iterable, Protocol, TypeVar, TypedDict, Union +from typing_extensions import NotRequired, Required, TypeAlias -import numpy +import datetime -AbbrParams: TypeAlias = dict[str, dict[str, object]] +from numpy import bool_ as Bool +from numpy import datetime64 as Date +from numpy import float32 as Float +from numpy import int16 as Enum +from numpy import int32 as Int +from numpy import str_ as String -FullParams: TypeAlias = dict[str, dict[str, AbbrParams]] +#: Generic type variables. +E = TypeVar("E") +G = TypeVar("G", covariant=True) +T = TypeVar("T", Bool, Date, Enum, Float, Int, String, covariant=True) +U = TypeVar("U", bool, datetime.date, float, str) +V = TypeVar("V", covariant=True) -AxesParams: TypeAlias = dict[str, list[list["AxisParams"]]] -EntsParams: TypeAlias = dict[str, Union[str, list[str]]] +#: Type alias for a simulation dictionary defining the roles. +Roles: TypeAlias = dict[str, Union[str, Iterable[str]]] -ImplParams: TypeAlias = Union[EntsParams, FullParams, AxesParams] +#: Type alias for a simulation dictionary with abbreviated entities. +Variables: TypeAlias = dict[str, dict[str, object]] -ExplParams: TypeAlias = Union[dict[str, EntsParams], FullParams, AxesParams] +#: Type alias for a simulation with fully specified single entities. +SingleEntities: TypeAlias = dict[str, dict[str, Variables]] -NoAxParams: TypeAlias = Union[dict[str, EntsParams], FullParams] +#: Type alias for a simulation dictionary with implicit group entities. +ImplicitGroupEntities: TypeAlias = dict[str, Union[Roles, Variables]] -Params: TypeAlias = Union[dict[str, EntsParams], FullParams, AxesParams, AbbrParams] +#: Type alias for a simulation dictionary with explicit group entities. +GroupEntities: TypeAlias = dict[str, ImplicitGroupEntities] +#: Type alias for a simulation dictionary with fully specified entities. +FullySpecifiedEntities: TypeAlias = Union[SingleEntities, GroupEntities] -class AxisParams(TypedDict, total=False): - name: str - count: int - min: float - max: float - period: str | int - index: int +#: Type alias for a simulation dictionary with axes parameters. +Axes: TypeAlias = dict[str, Iterable[Iterable["Axis"]]] + +#: Type alias for a simulation dictionary without axes parameters. +ParamsWithoutAxes: TypeAlias = Union[ + Variables, ImplicitGroupEntities, FullySpecifiedEntities +] + +#: Type alias for a simulation dictionary with axes parameters. +ParamsWithAxes: TypeAlias = Union[Axes, ParamsWithoutAxes] + +#: Type alias for a simulation dictionary with all the possible scenarios. +Params: TypeAlias = ParamsWithAxes + + +class Axis(TypedDict, total=False): + """Interface representing an axis of a simulation.""" + + count: Required[int] + index: NotRequired[int] + max: Required[float] + min: Required[float] + name: Required[str] + period: NotRequired[str | int] class Entity(Protocol): + """Interface representing an entity of a simulation.""" + key: str plural: str | None - @typing.overload def get_variable( self, - variable_name: str, - check_existence: bool = True, - ) -> Variable: - ... + __variable_name: str, + __check_existence: bool = ..., + ) -> Variable[T] | None: + """Get a variable.""" - @typing.overload - def get_variable( - self, - variable_name: str, - check_existence: bool = False, - ) -> Variable | None: - ... - def get_variable( - self, - variable_name: str, - check_existence: bool = ..., - ) -> Variable | None: - ... +class SingleEntity(Entity, Protocol): + """Interface representing a single entity of a simulation.""" -class SingleEntity(Entity): - ... +class GroupEntity(Entity, Protocol): + """Interface representing a group entity of a simulation.""" + @property + @abstractmethod + def flattened_roles(self) -> Iterable[Role[G]]: + """Get the flattened roles of the GroupEntity.""" -class GroupEntity(Entity): - flattened_roles: Iterable[Role] +class Holder(Protocol[V]): + """Interface representing a holder of a simulation's computed values.""" -class Holder(Protocol): - variable: Variable + @property + @abstractmethod + def variable(self) -> Variable[T]: + """Get the Variable of the Holder.""" def set_input( self, - period: Period, - array: Array[Any] | Sequence[object], - ) -> Array[Any] | None: - ... + __period: Period, + __array: Array[T] | Sequence[U], + ) -> Array[T] | None: + """Set values for a Variable for a given Period.""" class Period(Protocol): - ... + """Interface representing a period of a simulation.""" -class Role(Protocol): - ... +class Population(Protocol[E]): + """Interface representing a data vector of an Entity.""" - -class Population(Protocol): count: int - entity: Entity - ids: Array[numpy.str_] + entity: E + ids: Array[String] + + def get_holder(self, __variable_name: str) -> Holder[V]: + """Get the holder of a Variable.""" + - def get_holder(self, variable_name: str) -> Holder: - ... +class SinglePopulation(Population[E], Protocol): + """Interface representing a data vector of a SingleEntity.""" -class SinglePopulation(Population): - ... +class GroupPopulation(Population[E], Protocol): + """Interface representing a data vector of a GroupEntity.""" + members_entity_id: Array[String] -class GroupPopulation(Population): - def nb_persons(self, role: Role | None = ...) -> int: - ... + def nb_persons(self, __role: Role[G] | None = ...) -> int: + """Get the number of persons for a given Role.""" + + +class Role(Protocol[G]): + """Interface representing a role of the group entities of a simulation.""" class TaxBenefitSystem(Protocol): - person_entity: SingleEntity - variables: dict[str, Variable] + """Interface representing a tax-benefit system.""" + + @property + @abstractmethod + def person_entity(self) -> SingleEntity: + """Get the person entity of the tax-benefit system.""" - def entities_by_singular(self) -> Iterable[str]: - ... + @person_entity.setter + @abstractmethod + def person_entity(self, person_entity: SingleEntity) -> None: + """Set the person entity of the tax-benefit system.""" + + @property + @abstractmethod + def variables(self) -> dict[str, V]: + """Get the variables of the tax-benefit system.""" + + def entities_by_singular(self) -> dict[str, E]: + """Get the singular form of the entities' keys.""" def entities_plural(self) -> Iterable[str]: - ... + """Get the plural form of the entities' keys.""" + + def get_variable( + self, + __variable_name: str, + __check_existence: bool = ..., + ) -> V | None: + """Get a variable.""" def instantiate_entities( self, - ) -> dict[str, Union[SinglePopulation, GroupPopulation]]: - ... + ) -> dict[str, Population[E]]: + """Instantiate the populations of each Entity.""" + +class Variable(Protocol[T]): + """Interface representing a variable of a tax-benefit system.""" -class Variable(Protocol): end: str - def default_array(self, array_size: int) -> Array[Any]: - ... + def default_array(self, __array_size: int) -> Array[T]: + """Fill an array with the default value of the Variable.""" From de2ec433ada135dd87392d2ff0b9bd3b09db85c6 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Dec 2023 07:47:12 +0100 Subject: [PATCH 057/188] doc: document type guards --- .../errors/situation_parsing_error.py | 10 +- .../simulations/_build_default_simulation.py | 59 ++-- .../simulations/_build_from_variables.py | 232 +++++++++++++ openfisca_core/simulations/_type_guards.py | 298 ++++++++++++++++- openfisca_core/simulations/helpers.py | 99 +++++- openfisca_core/simulations/simulation.py | 4 +- .../simulations/simulation_builder.py | 307 +++++++++++------- openfisca_core/simulations/typing.py | 15 +- 8 files changed, 844 insertions(+), 180 deletions(-) create mode 100644 openfisca_core/simulations/_build_from_variables.py diff --git a/openfisca_core/errors/situation_parsing_error.py b/openfisca_core/errors/situation_parsing_error.py index 7b68430dbb..ff3839d5f7 100644 --- a/openfisca_core/errors/situation_parsing_error.py +++ b/openfisca_core/errors/situation_parsing_error.py @@ -1,3 +1,7 @@ +from __future__ import annotations + +from collections.abc import Iterable + import os import dpath.util @@ -8,7 +12,9 @@ class SituationParsingError(Exception): Exception raised when the situation provided as an input for a simulation cannot be parsed """ - def __init__(self, path, message, code=None): + def __init__( + self, path: Iterable[str], message: str, code: int | None = None + ) -> None: self.error = {} dpath_path = "/".join([str(item) for item in path]) message = str(message).strip(os.linesep).replace(os.linesep, " ") @@ -16,5 +22,5 @@ def __init__(self, path, message, code=None): self.code = code Exception.__init__(self, str(self.error)) - def __str__(self): + def __str__(self) -> str: return str(self.error) diff --git a/openfisca_core/simulations/_build_default_simulation.py b/openfisca_core/simulations/_build_default_simulation.py index 7ce4009ed1..f99c1d210a 100644 --- a/openfisca_core/simulations/_build_default_simulation.py +++ b/openfisca_core/simulations/_build_default_simulation.py @@ -17,15 +17,13 @@ class _BuildDefaultSimulation: count(int): The number of periods. Examples: - >>> from openfisca_core.entities import Entity as SingleEntity - >>> from openfisca_core.entities import GroupEntity - >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + >>> from openfisca_core import entities, taxbenefitsystems >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} - >>> single_entity = SingleEntity("dog", "dogs", "", "") - >>> group_entity = GroupEntity("pack", "packs", "", "", [role]) - >>> entities = {single_entity, group_entity} - >>> tax_benefit_system = TaxBenefitSystem(entities) + >>> single_entity = entities.Entity("dog", "dogs", "", "") + >>> group_entity = entities.GroupEntity("pack", "packs", "", "", [role]) + >>> test_entities = [single_entity, group_entity] + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem(test_entities) >>> count = 1 >>> builder = ( ... _BuildDefaultSimulation(tax_benefit_system, count) @@ -43,9 +41,6 @@ class _BuildDefaultSimulation: >>> sorted(builder.simulation.populations.keys()) ['dog', 'pack'] - >>> sorted(builder.tax_benefit_system.entities_by_singular().keys()) - ['dog', 'pack'] - """ #: The number of Population. @@ -57,14 +52,8 @@ class _BuildDefaultSimulation: #: The built simulation. simulation: Simulation - #: The tax-benefit system. - tax_benefit_system: TaxBenefitSystem - - def __init__( - self, tax_benefit_system: TaxBenefitSystem, count: int - ) -> None: + def __init__(self, tax_benefit_system: TaxBenefitSystem, count: int) -> None: self.count = count - self.tax_benefit_system = tax_benefit_system self.populations = tax_benefit_system.instantiate_entities() self.simulation = Simulation(tax_benefit_system, self.populations) @@ -75,15 +64,13 @@ def add_count(self) -> Self: _BuildDefaultSimulation: The builder. Examples: - >>> from openfisca_core.entities import Entity as SingleEntity - >>> from openfisca_core.entities import GroupEntity - >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + >>> from openfisca_core import entities, taxbenefitsystems >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} - >>> single_entity = SingleEntity("dog", "dogs", "", "") - >>> group_entity = GroupEntity("pack", "packs", "", "", [role]) - >>> entities = {single_entity, group_entity} - >>> tax_benefit_system = TaxBenefitSystem(entities) + >>> single_entity = entities.Entity("dog", "dogs", "", "") + >>> group_entity = entities.GroupEntity("pack", "packs", "", "", [role]) + >>> test_entities = [single_entity, group_entity] + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem(test_entities) >>> count = 2 >>> builder = _BuildDefaultSimulation(tax_benefit_system, count) @@ -110,15 +97,13 @@ def add_ids(self) -> Self: _BuildDefaultSimulation: The builder. Examples: - >>> from openfisca_core.entities import Entity as SingleEntity - >>> from openfisca_core.entities import GroupEntity - >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + >>> from openfisca_core import entities, taxbenefitsystems >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} - >>> single_entity = SingleEntity("dog", "dogs", "", "") - >>> group_entity = GroupEntity("pack", "packs", "", "", [role]) - >>> entities = {single_entity, group_entity} - >>> tax_benefit_system = TaxBenefitSystem(entities) + >>> single_entity = entities.Entity("dog", "dogs", "", "") + >>> group_entity = entities.GroupEntity("pack", "packs", "", "", [role]) + >>> test_entities = [single_entity, group_entity] + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem(test_entities) >>> count = 2 >>> builder = _BuildDefaultSimulation(tax_benefit_system, count) @@ -147,15 +132,13 @@ def add_members_entity_id(self) -> Self: _BuildDefaultSimulation: The builder. Examples: - >>> from openfisca_core.entities import Entity as SingleEntity - >>> from openfisca_core.entities import GroupEntity - >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + >>> from openfisca_core import entities, taxbenefitsystems >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} - >>> single_entity = SingleEntity("dog", "dogs", "", "") - >>> group_entity = GroupEntity("pack", "packs", "", "", [role]) - >>> entities = {single_entity, group_entity} - >>> tax_benefit_system = TaxBenefitSystem(entities) + >>> single_entity = entities.Entity("dog", "dogs", "", "") + >>> group_entity = entities.GroupEntity("pack", "packs", "", "", [role]) + >>> test_entities = [single_entity, group_entity] + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem(test_entities) >>> count = 2 >>> builder = _BuildDefaultSimulation(tax_benefit_system, count) diff --git a/openfisca_core/simulations/_build_from_variables.py b/openfisca_core/simulations/_build_from_variables.py new file mode 100644 index 0000000000..60ff6148e7 --- /dev/null +++ b/openfisca_core/simulations/_build_from_variables.py @@ -0,0 +1,232 @@ +"""This module contains the _BuildFromVariables class.""" + +from __future__ import annotations + +from typing_extensions import Self + +from openfisca_core import errors + +from ._build_default_simulation import _BuildDefaultSimulation +from ._type_guards import is_variable_dated +from .simulation import Simulation +from .typing import Entity, Population, TaxBenefitSystem, Variables + + +class _BuildFromVariables: + """Build a simulation from variables. + + Args: + tax_benefit_system(TaxBenefitSystem): The tax-benefit system. + params(Variables): The simulation parameters. + + Examples: + >>> from openfisca_core import entities, periods, taxbenefitsystems, variables + + >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} + >>> single_entity = entities.Entity("dog", "dogs", "", "") + >>> group_entity = entities.GroupEntity("pack", "packs", "", "", [role]) + + >>> class salary(variables.Variable): + ... definition_period = periods.DateUnit.MONTH + ... entity = single_entity + ... value_type = int + + >>> class taxes(variables.Variable): + ... definition_period = periods.DateUnit.MONTH + ... entity = group_entity + ... value_type = int + + >>> test_entities = [single_entity, group_entity] + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem(test_entities) + >>> tax_benefit_system.load_variable(salary) + <...salary object at ...> + >>> tax_benefit_system.load_variable(taxes) + <...taxes object at ...> + >>> period = "2023-12" + >>> variables = {"salary": {period: 10000}, "taxes": 5000} + >>> builder = ( + ... _BuildFromVariables(tax_benefit_system, variables, period) + ... .add_dated_values() + ... .add_undated_values() + ... ) + + >>> dogs = builder.populations["dog"].get_holder("salary") + >>> dogs.get_array(period) + array([10000], dtype=int32) + + >>> pack = builder.populations["pack"].get_holder("taxes") + >>> pack.get_array(period) + array([5000], dtype=int32) + + """ + + #: The number of Population. + count: int + + #: The Simulation's default period. + default_period: str | None + + #: The built populations. + populations: dict[str, Population[Entity]] + + #: The built simulation. + simulation: Simulation + + #: The simulation parameters. + variables: Variables + + def __init__( + self, + tax_benefit_system: TaxBenefitSystem, + params: Variables, + default_period: str | None = None, + ) -> None: + self.count = _person_count(params) + + default_builder = ( + _BuildDefaultSimulation(tax_benefit_system, self.count) + .add_count() + .add_ids() + .add_members_entity_id() + ) + + self.variables = params + self.simulation = default_builder.simulation + self.populations = default_builder.populations + self.default_period = default_period + + def add_dated_values(self) -> Self: + """Add the dated input values to the Simulation. + + Returns: + _BuildFromVariables: The builder. + + Examples: + >>> from openfisca_core import entities, periods, taxbenefitsystems, variables + + >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} + >>> single_entity = entities.Entity("dog", "dogs", "", "") + >>> group_entity = entities.GroupEntity("pack", "packs", "", "", [role]) + + + >>> class salary(variables.Variable): + ... definition_period = periods.DateUnit.MONTH + ... entity = single_entity + ... value_type = int + + >>> class taxes(variables.Variable): + ... definition_period = periods.DateUnit.MONTH + ... entity = group_entity + ... value_type = int + + >>> test_entities = [single_entity, group_entity] + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem(test_entities) + >>> tax_benefit_system.load_variable(salary) + <...salary object at ...> + >>> tax_benefit_system.load_variable(taxes) + <...taxes object at ...> + >>> period = "2023-12" + >>> variables = {"salary": {period: 10000}, "taxes": 5000} + >>> builder = _BuildFromVariables(tax_benefit_system, variables) + >>> builder.add_dated_values() + <..._BuildFromVariables object at ...> + + >>> dogs = builder.populations["dog"].get_holder("salary") + >>> dogs.get_array(period) + array([10000], dtype=int32) + + >>> pack = builder.populations["pack"].get_holder("taxes") + >>> pack.get_array(period) + + """ + + for variable, value in self.variables.items(): + if is_variable_dated(dated_variable := value): + for period, dated_value in dated_variable.items(): + self.simulation.set_input(variable, period, dated_value) + + return self + + def add_undated_values(self) -> Self: + """Add the undated input values to the Simulation. + + Returns: + _BuildFromVariables: The builder. + + Raises: + SituationParsingError: If there is not a default period set. + + Examples: + >>> from openfisca_core import entities, periods, taxbenefitsystems, variables + + >>> role = {"key": "stray", "plural": "stray", "label": "", "doc": ""} + >>> single_entity = entities.Entity("dog", "dogs", "", "") + >>> group_entity = entities.GroupEntity("pack", "packs", "", "", [role]) + + >>> class salary(variables.Variable): + ... definition_period = periods.DateUnit.MONTH + ... entity = single_entity + ... value_type = int + + >>> class taxes(variables.Variable): + ... definition_period = periods.DateUnit.MONTH + ... entity = group_entity + ... value_type = int + + >>> test_entities = [single_entity, group_entity] + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem(test_entities) + >>> tax_benefit_system.load_variable(salary) + <...salary object at ...> + >>> tax_benefit_system.load_variable(taxes) + <...taxes object at ...> + >>> period = "2023-12" + >>> variables = {"salary": {period: 10000}, "taxes": 5000} + >>> builder = _BuildFromVariables(tax_benefit_system, variables) + >>> builder.add_undated_values() + Traceback (most recent call last): + openfisca_core.errors.situation_parsing_error.SituationParsingError + >>> builder.default_period = period + >>> builder.add_undated_values() + <..._BuildFromVariables object at ...> + + >>> dogs = builder.populations["dog"].get_holder("salary") + >>> dogs.get_array(period) + + >>> pack = builder.populations["pack"].get_holder("taxes") + >>> pack.get_array(period) + array([5000], dtype=int32) + + """ + + for variable, value in self.variables.items(): + if not is_variable_dated(undated_value := value): + if (period := self.default_period) is None: + message = ( + "Can't deal with type: expected object. Input " + "variables should be set for specific periods. For " + "instance: " + " {'salary': {'2017-01': 2000, '2017-02': 2500}}" + " {'birth_date': {'ETERNITY': '1980-01-01'}}" + ) + + raise errors.SituationParsingError([variable], message) + + self.simulation.set_input(variable, period, undated_value) + + return self + + +def _person_count(params: Variables) -> int: + try: + first_value = next(iter(params.values())) + + if isinstance(first_value, dict): + first_value = next(iter(first_value.values())) + + if isinstance(first_value, str): + return 1 + + return len(first_value) + + except Exception: + return 1 diff --git a/openfisca_core/simulations/_type_guards.py b/openfisca_core/simulations/_type_guards.py index b68e36d8e2..c34361041a 100644 --- a/openfisca_core/simulations/_type_guards.py +++ b/openfisca_core/simulations/_type_guards.py @@ -1,22 +1,304 @@ +"""Type guards to help type narrowing simulation parameters.""" + from __future__ import annotations from typing import Iterable from typing_extensions import TypeGuard -from .typing import AbbrParams, AxesParams, ExplParams, ImplParams, Params +from .typing import ( + Axes, + DatedVariable, + FullySpecifiedEntities, + ImplicitGroupEntities, + Params, + UndatedVariable, + Variables, +) + + +def are_entities_fully_specified( + params: Params, items: Iterable[str] +) -> TypeGuard[FullySpecifiedEntities]: + """Check if the params contain fully specified entities. + + Args: + params(Params): Simulation parameters. + items(Iterable[str]): List of entities in plural form. + + Returns: + bool: True if the params contain fully specified entities. + + Examples: + >>> entities = {"persons", "households"} + + >>> params = { + ... "axes": [ + ... [{"count": 2, "max": 3000, "min": 0, "name": "rent", "period": "2018-11"}] + ... ], + ... "households": { + ... "housea": {"parents": ["Alicia", "Javier"]}, + ... "houseb": {"parents": ["Tom"]}, + ... }, + ... "persons": {"Alicia": {"salary": {"2018-11": 0}}, "Javier": {}, "Tom": {}}, + ... } + + >>> are_entities_fully_specified(params, entities) + True + + >>> params = { + ... "persons": {"Javier": {"salary": {"2018-11": [2000, 3000]}}} + ... } + + >>> are_entities_fully_specified(params, entities) + True + + >>> params = { + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}} + ... } + + >>> are_entities_fully_specified(params, entities) + True + + >>> params = {"household": {"parents": ["Javier"]}} + + >>> are_entities_fully_specified(params, entities) + False + + >>> params = {"salary": {"2016-10": 12000}} + + >>> are_entities_fully_specified(params, entities) + False + + >>> params = {"salary": 12000} + + >>> are_entities_fully_specified(params, entities) + False + + >>> params = {} + + >>> are_entities_fully_specified(params, entities) + False + + """ + + if not params: + return False + + return all(key in items for key in params.keys() if key != "axes") + + +def are_entities_short_form( + params: Params, items: Iterable[str] +) -> TypeGuard[ImplicitGroupEntities]: + """Check if the params contain short form entities. + + Args: + params(Params): Simulation parameters. + items(Iterable[str]): List of entities in singular form. + + Returns: + bool: True if the params contain short form entities. + + Examples: + >>> entities = {"person", "household"} + + >>> params = { + ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "households": {"household": {"parents": ["Javier"]}}, + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... } + + >>> are_entities_short_form(params, entities) + False + + >>> params = { + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}} + ... } + + >>> are_entities_short_form(params, entities) + False + + >>> params = { + ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "household": {"parents": ["Javier"]}, + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... } + + >>> are_entities_short_form(params, entities) + True + + >>> params = { + ... "household": {"parents": ["Javier"]}, + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... } + + >>> are_entities_short_form(params, entities) + True + + >>> params = {"household": {"parents": ["Javier"]}} + + >>> are_entities_short_form(params, entities) + True + + >>> params = {"household": {"parents": "Javier"}} + + >>> are_entities_short_form(params, entities) + True + + >>> params = {"salary": {"2016-10": 12000}} + + >>> are_entities_short_form(params, entities) + False + + >>> params = {"salary": 12000} + + >>> are_entities_short_form(params, entities) + False + >>> params = {} -def is_abbr_spec(params: Params, items: Iterable[str]) -> TypeGuard[AbbrParams]: - return any(key in items for key in params.keys()) or params == {} + >>> are_entities_short_form(params, entities) + False + """ -def is_impl_spec(params: Params, items: Iterable[str]) -> TypeGuard[ImplParams]: - return set(params).intersection(items) + return not not set(params).intersection(items) -def is_expl_spec(params: Params, items: Iterable[str]) -> TypeGuard[ExplParams]: - return any(key in items for key in params.keys()) +def are_entities_specified( + params: Params, items: Iterable[str] +) -> TypeGuard[Variables]: + """Check if the params contains entities at all. + Args: + params(Params): Simulation parameters. + items(Iterable[str]): List of variables. + + Returns: + bool: True if the params does not contain variables at the root level. + + Examples: + >>> variables = {"salary"} + + >>> params = { + ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "households": {"household": {"parents": ["Javier"]}}, + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... } + + >>> are_entities_specified(params, variables) + True + + >>> params = { + ... "persons": {"Javier": {"salary": {"2018-11": [2000, 3000]}}} + ... } + + >>> are_entities_specified(params, variables) + True + + >>> params = { + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}} + ... } + + >>> are_entities_specified(params, variables) + True + + >>> params = {"household": {"parents": ["Javier"]}} + + >>> are_entities_specified(params, variables) + True + + >>> params = {"salary": {"2016-10": [12000, 13000]}} + + >>> are_entities_specified(params, variables) + False + + >>> params = {"salary": {"2016-10": 12000}} + + >>> are_entities_specified(params, variables) + False + + >>> params = {"salary": [12000, 13000]} + + >>> are_entities_specified(params, variables) + False + + >>> params = {"salary": 12000} + + >>> are_entities_specified(params, variables) + False + + >>> params = {} + + >>> are_entities_specified(params, variables) + False + + """ + + if not params: + return False + + return not any(key in items for key in params.keys()) + + +def has_axes(params: Params) -> TypeGuard[Axes]: + """Check if the params contains axes. + + Args: + params(Params): Simulation parameters. + + Returns: + bool: True if the params contain axes. + + Examples: + >>> params = { + ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "households": {"household": {"parents": ["Javier"]}}, + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... } + + >>> has_axes(params) + True + + >>> params = { + ... "persons": {"Javier": {"salary": {"2018-11": [2000, 3000]}}} + ... } + + >>> has_axes(params) + False + + """ -def is_axes_spec(params: ExplParams | Params) -> TypeGuard[AxesParams]: return params.get("axes", None) is not None + + +def is_variable_dated( + variable: DatedVariable | UndatedVariable, +) -> TypeGuard[DatedVariable]: + """Check if the variable is dated. + + Args: + variable(DatedVariable | UndatedVariable): A variable. + + Returns: + bool: True if the variable is dated. + + Examples: + >>> variable = {"2018-11": [2000, 3000]} + + >>> is_variable_dated(variable) + True + + >>> variable = {"2018-11": 2000} + + >>> is_variable_dated(variable) + True + + >>> variable = 2000 + + >>> is_variable_dated(variable) + False + + """ + + return isinstance(variable, dict) diff --git a/openfisca_core/simulations/helpers.py b/openfisca_core/simulations/helpers.py index b559f7d071..d5984d88b6 100644 --- a/openfisca_core/simulations/helpers.py +++ b/openfisca_core/simulations/helpers.py @@ -1,4 +1,8 @@ -from openfisca_core.errors import SituationParsingError +from collections.abc import Iterable + +from openfisca_core import errors + +from .typing import ParamsWithoutAxes def calculate_output_add(simulation, variable_name: str, period): @@ -20,28 +24,93 @@ def check_type(input, input_type, path=None): path = [] if not isinstance(input, input_type): - raise SituationParsingError( + raise errors.SituationParsingError( path, "Invalid type: must be of type '{}'.".format(json_type_map[input_type]), ) +def check_unexpected_entities( + params: ParamsWithoutAxes, entities: Iterable[str] +) -> None: + """Check if the input contains entities that are not in the system. + + Args: + params(ParamsWithoutAxes): Simulation parameters. + entities(Iterable[str]): List of entities in plural form. + + Raises: + SituationParsingError: If there are entities that are not in the system. + + Examples: + >>> entities = {"persons", "households"} + + >>> params = { + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, + ... "households": {"household": {"parents": ["Javier"]}} + ... } + + >>> check_unexpected_entities(params, entities) + + >>> params = { + ... "dogs": {"Bart": {"damages": {"2018-11": 2000}}} + ... } + + >>> check_unexpected_entities(params, entities) + Traceback (most recent call last): + openfisca_core.errors.situation_parsing_error.SituationParsingError + + """ + + if has_unexpected_entities(params, entities): + unexpected_entities = [entity for entity in params if entity not in entities] + + message = ( + "Some entities in the situation are not defined in the loaded tax " + "and benefit system. " + f"These entities are not found: {', '.join(unexpected_entities)}. " + f"The defined entities are: {', '.join(entities)}." + ) + + raise errors.SituationParsingError([unexpected_entities[0]], message) + + +def has_unexpected_entities(params: ParamsWithoutAxes, entities: Iterable[str]) -> bool: + """Check if the input contains entities that are not in the system. + + Args: + params(ParamsWithoutAxes): Simulation parameters. + entities(Iterable[str]): List of entities in plural form. + + Returns: + bool: True if the input contains entities that are not in the system. + + Examples: + >>> entities = {"persons", "households"} + + >>> params = { + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, + ... "households": {"household": {"parents": ["Javier"]}} + ... } + + >>> has_unexpected_entities(params, entities) + False + + >>> params = { + ... "dogs": {"Bart": {"damages": {"2018-11": 2000}}} + ... } + + >>> has_unexpected_entities(params, entities) + True + + """ + + return any(entity for entity in params if entity not in entities) + + def transform_to_strict_syntax(data): if isinstance(data, (str, int)): data = [data] if isinstance(data, list): return [str(item) if isinstance(item, int) else item for item in data] return data - - -def _get_person_count(input_dict): - try: - first_value = next(iter(input_dict.values())) - if isinstance(first_value, dict): - first_value = next(iter(first_value.values())) - if isinstance(first_value, str): - return 1 - - return len(first_value) - except Exception: - return 1 diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 304d7338ab..9e7e0034ea 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,7 +1,7 @@ from __future__ import annotations from openfisca_core.types import Population, TaxBenefitSystem, Variable -from typing import Dict, NamedTuple, Optional, Set +from typing import Dict, Mapping, NamedTuple, Optional, Set import tempfile import warnings @@ -24,7 +24,7 @@ class Simulation: def __init__( self, tax_benefit_system: TaxBenefitSystem, - populations: Dict[str, Population], + populations: Mapping[str, Population], ): """ This constructor is reserved for internal use; see :any:`SimulationBuilder`, diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 365bf9db05..c42d0e4f22 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -1,6 +1,5 @@ from __future__ import annotations -import typing from collections.abc import Iterable, Sequence from numpy.typing import NDArray as Array from typing import Dict, List @@ -13,20 +12,29 @@ from openfisca_core import entities, errors, periods, populations, variables from . import helpers -from ._type_guards import is_abbr_spec, is_axes_spec, is_expl_spec, is_impl_spec +from ._build_default_simulation import _BuildDefaultSimulation +from ._build_from_variables import _BuildFromVariables +from ._type_guards import ( + are_entities_fully_specified, + are_entities_short_form, + are_entities_specified, + has_axes, +) from .simulation import Simulation from .typing import ( - AbbrParams, - AxisParams, + Axis, Entity, - ExplParams, + FullySpecifiedEntities, + GroupEntities, GroupEntity, - ImplParams, - NoAxParams, + ImplicitGroupEntities, Params, + ParamsWithoutAxes, + Population, Role, SingleEntity, TaxBenefitSystem, + Variables, ) @@ -66,87 +74,127 @@ def build_from_dict( tax_benefit_system: TaxBenefitSystem, input_dict: Params, ) -> Simulation: - """ - Build a simulation from ``input_dict`` + """Build a simulation from an input dictionary. + + This method uses :meth:`.SimulationBuilder.build_from_entities` if + entities are fully specified, or + :meth:`.SimulationBuilder.build_from_variables` if they are not. + + Args: + tax_benefit_system(TaxBenefitSystem): The system to use. + input_dict(Params): The input of the simulation. + + Returns: + Simulation: The built simulation. + + Examples: + >>> entities = {"person", "household"} + + >>> params = { + ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "household": {"parents": ["Javier"]}, + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... } + + >>> are_entities_short_form(params, entities) + True + + >>> entities = {"persons", "households"} + + >>> params = { + ... "axes": [ + ... [{"count": 2, "max": 3000, "min": 0, "name": "rent", "period": "2018-11"}] + ... ], + ... "households": { + ... "housea": {"parents": ["Alicia", "Javier"]}, + ... "houseb": {"parents": ["Tom"]}, + ... }, + ... "persons": {"Alicia": {"salary": {"2018-11": 0}}, "Javier": {}, "Tom": {}}, + ... } + + >>> are_entities_short_form(params, entities) + True - This method uses :any:`build_from_entities` if entities are fully specified, or :any:`build_from_variables` if not. + >>> params = {"salary": [12000, 13000]} + + >>> not are_entities_specified(params, {"salary"}) + True - :param dict input_dict: A dict represeting the input of the simulation - :return: A :any:`Simulation` """ - variables = tax_benefit_system.variables.keys() + #: The plural names of the entities in the tax and benefits system. + plural: Iterable[str] = tax_benefit_system.entities_plural() - singular = tax_benefit_system.entities_by_singular() + #: The singular names of the entities in the tax and benefits system. + singular: Iterable[str] = tax_benefit_system.entities_by_singular() - plural = tax_benefit_system.entities_plural() + #: The names of the variables in the tax and benefits system. + variables: Iterable[str] = tax_benefit_system.variables.keys() - if is_impl_spec(input_dict, singular): - expl = self.explicit_singular_entities(tax_benefit_system, input_dict) - return self.build_from_entities(tax_benefit_system, expl) + if are_entities_short_form(input_dict, singular): + params = self.explicit_singular_entities(tax_benefit_system, input_dict) + return self.build_from_entities(tax_benefit_system, params) - if is_expl_spec(input_dict, plural): - return self.build_from_entities(tax_benefit_system, input_dict) + if are_entities_fully_specified(params := input_dict, plural): + return self.build_from_entities(tax_benefit_system, params) - if is_abbr_spec(input_dict, variables): - return self.build_from_variables(tax_benefit_system, input_dict) + if not are_entities_specified(params := input_dict, variables): + return self.build_from_variables(tax_benefit_system, params) def build_from_entities( self, tax_benefit_system: TaxBenefitSystem, - input_dict: ExplParams, + input_dict: FullySpecifiedEntities, ) -> Simulation: - """ - Build a simulation from a Python dict ``input_dict`` fully specifying entities. + """Build a simulation from a Python dict ``input_dict`` fully specifying + entities. Examples: + >>> entities = {"person", "household"} - >>> { - ... 'persons': {'Javier': { 'salary': {'2018-11': 2000}}}, - ... 'households': {'household': {'parents': ['Javier']}} + >>> params = { + ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "household": {"parents": ["Javier"]}, + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] ... } + + >>> are_entities_short_form(params, entities) + True + """ + + # Create the populations + populations = tax_benefit_system.instantiate_entities() + + # Create the simulation + simulation = Simulation(tax_benefit_system, populations) + + # Why? input_dict = copy.deepcopy(input_dict) - simulation = Simulation( - tax_benefit_system, tax_benefit_system.instantiate_entities() - ) + # The plural names of the entities in the tax and benefits system. + plural: Iterable[str] = tax_benefit_system.entities_plural() # Register variables so get_variable_entity can find them - for variable_name, _variable in tax_benefit_system.variables.items(): - self.register_variable( - variable_name, simulation.get_variable_population(variable_name).entity - ) + self.register_variables(simulation) - helpers.check_type(input_dict, dict, ["error"]) + # Declare axes + axes: list[list[Axis]] | None = None - axes: list[list[AxisParams]] | None = None + # ? + helpers.check_type(input_dict, dict, ["error"]) - if is_axes_spec(input_dict): - axes = input_dict.pop("axes") + # Remove axes from input_dict + params: ParamsWithoutAxes = { + key: value for key, value in input_dict.items() if key != "axes" + } - params: NoAxParams = typing.cast(NoAxParams, input_dict) + # Save axes for later + if has_axes(axes_params := input_dict): + axes = copy.deepcopy(axes_params.get("axes", None)) - unexpected_entities = [ - entity - for entity in params - if entity not in tax_benefit_system.entities_plural() - ] - if unexpected_entities: - unexpected_entity = unexpected_entities[0] - raise errors.SituationParsingError( - [unexpected_entity], - "".join( - [ - "Some entities in the situation are not defined in the loaded tax and benefit system.", - "These entities are not found: {0}.", - "The defined entities are: {1}.", - ] - ).format( - ", ".join(unexpected_entities), - ", ".join(tax_benefit_system.entities_plural()), - ), - ) + # Check for unexpected entities + helpers.check_unexpected_entities(params, plural) person_entity: SingleEntity = tax_benefit_system.person_entity @@ -170,20 +218,21 @@ def build_from_entities( self.persons_plural, persons_ids, entity_class, instances_json ) - elif axes: - raise errors.SituationParsingError( - [entity_class.plural], + elif axes is not None: + message = ( f"We could not find any specified {entity_class.plural}. " "In order to expand over axes, all group entities and roles " "must be fully specified. For further support, please do " "not hesitate to take a look at the official documentation: " - "https://openfisca.org/doc/simulate/replicate-simulation-inputs.html.", + "https://openfisca.org/doc/simulate/replicate-simulation-inputs.html." ) + raise errors.SituationParsingError([entity_class.plural], message) + else: self.add_default_group_entity(persons_ids, entity_class) - if axes: + if axes is not None: for axis in axes[0]: self.add_parallel_axis(axis) @@ -208,51 +257,64 @@ def build_from_entities( return simulation def build_from_variables( - self, tax_benefit_system: TaxBenefitSystem, input_dict: AbbrParams + self, tax_benefit_system: TaxBenefitSystem, input_dict: Variables ) -> Simulation: - """ - Build a simulation from a Python dict ``input_dict`` describing variables values without expliciting entities. + """Build a simulation from a Python dict ``input_dict`` describing + variables values without expliciting entities. - This method uses :any:`build_default_simulation` to infer an entity structure + This method uses :meth:`.SimulationBuilder.build_default_simulation` to + infer an entity structure. - Example: - >>> {'salary': {'2016-10': 12000}} + Args: + tax_benefit_system(TaxBenefitSystem): The system to use. + input_dict(Variables): The input of the simulation. - """ - count = helpers._get_person_count(input_dict) - simulation = self.build_default_simulation(tax_benefit_system, count) - for variable, value in input_dict.items(): - if not isinstance(value, dict): - if self.default_period is None: - raise errors.SituationParsingError( - [variable], - "Can't deal with type: expected object. Input variables should be set for specific periods. For instance: {'salary': {'2017-01': 2000, '2017-02': 2500}}, or {'birth_date': {'ETERNITY': '1980-01-01'}}.", - ) - simulation.set_input(variable, self.default_period, value) - else: - for period_str, dated_value in value.items(): - simulation.set_input(variable, period_str, dated_value) - return simulation + Returns: + Simulation: The built simulation. + + Raises: + SituationParsingError: If the input is not valid. + + Examples: + >>> params = {'salary': {'2016-10': 12000}} + + >>> are_entities_specified(params, {"salary"}) + False + + >>> params = {'salary': 12000} + + >>> are_entities_specified(params, {"salary"}) + False - def build_default_simulation(self, tax_benefit_system, count=1): """ - Build a simulation where: + + return ( + _BuildFromVariables(tax_benefit_system, input_dict, self.default_period) + .add_dated_values() + .add_undated_values() + .simulation + ) + + @staticmethod + def build_default_simulation( + tax_benefit_system: TaxBenefitSystem, count: int = 1 + ) -> Simulation: + """Build a default simulation. + + Where: - There are ``count`` persons - - There are ``count`` instances of each group entity, containing one person + - There are ``count`` of each group entity, containing one person - Every person has, in each entity, the first role + """ - simulation = Simulation( - tax_benefit_system, tax_benefit_system.instantiate_entities() + return ( + _BuildDefaultSimulation(tax_benefit_system, count) + .add_count() + .add_ids() + .add_members_entity_id() + .simulation ) - for population in simulation.populations.values(): - population.count = count - population.ids = numpy.array(range(count)) - if not population.entity.is_person: - population.members_entity_id = ( - population.ids - ) # Each person is its own group entity - return simulation def create_entities(self, tax_benefit_system): self.populations = tax_benefit_system.instantiate_entities() @@ -301,17 +363,29 @@ def build(self, tax_benefit_system): return Simulation(tax_benefit_system, self.populations) def explicit_singular_entities( - self, tax_benefit_system: TaxBenefitSystem, input_dict: ImplParams - ) -> ExplParams: - """ - Preprocess ``input_dict`` to explicit entities defined using the single-entity shortcut + self, tax_benefit_system: TaxBenefitSystem, input_dict: ImplicitGroupEntities + ) -> GroupEntities: + """Preprocess ``input_dict`` to explicit entities defined using the + single-entity shortcut + + Examples: - Example: + >>> params = {'persons': {'Javier': {}, }, 'household': {'parents': ['Javier']}} + + >>> are_entities_fully_specified(params, {"persons", "households"}) + False + + >>> are_entities_short_form(params, {"person", "household"}) + True + + >>> params = {'persons': {'Javier': {}}, 'households': {'household': {'parents': ['Javier']}}} + + >>> are_entities_fully_specified(params, {"persons", "households"}) + True + + >>> are_entities_short_form(params, {"person", "household"}) + False - >>> simulation_builder.explicit_singular_entities( - {'persons': {'Javier': {}, }, 'household': {'parents': ['Javier']}} - ) - >>> {'persons': {'Javier': {}}, 'households': {'household': {'parents': ['Javier']}} """ singular_keys = set(input_dict).intersection( @@ -616,22 +690,22 @@ def get_roles(self, entity_name: str) -> Sequence[Role]: # Return empty array for the "persons" entity return self.axes_roles.get(entity_name, self.roles.get(entity_name, [])) - def add_parallel_axis(self, axis: AxisParams) -> None: + def add_parallel_axis(self, axis: Axis) -> None: # All parallel axes have the same count and entity. # Search for a compatible axis, if none exists, error out self.axes[0].append(axis) - def add_perpendicular_axis(self, axis: AxisParams) -> None: + def add_perpendicular_axis(self, axis: Axis) -> None: # This adds an axis perpendicular to all previous dimensions self.axes.append([axis]) def expand_axes(self) -> None: # This method should be idempotent & allow change in axes - perpendicular_dimensions: list[list[AxisParams]] = self.axes + perpendicular_dimensions: list[list[Axis]] = self.axes cell_count: int = 1 for parallel_axes in perpendicular_dimensions: - first_axis: AxisParams = parallel_axes[0] + first_axis: Axis = parallel_axes[0] axis_count: int = first_axis["count"] cell_count *= axis_count @@ -734,3 +808,12 @@ def get_variable_entity(self, variable_name: str) -> Entity: def register_variable(self, variable_name: str, entity: Entity) -> None: self.variable_entities[variable_name] = entity + + def register_variables(self, simulation: Simulation) -> None: + tax_benefit_system: TaxBenefitSystem = simulation.tax_benefit_system + variables: Iterable[str] = tax_benefit_system.variables.keys() + + for name in variables: + population: Population = simulation.get_variable_population(name) + entity: Entity = population.entity + self.register_variable(name, entity) diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py index 6bf31ec1ac..18fa797c4a 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/typing.py @@ -3,9 +3,9 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Sequence +from collections.abc import Iterable, Sequence from numpy.typing import NDArray as Array -from typing import Iterable, Protocol, TypeVar, TypedDict, Union +from typing import Protocol, TypeVar, TypedDict, Union from typing_extensions import NotRequired, Required, TypeAlias import datetime @@ -28,8 +28,14 @@ #: Type alias for a simulation dictionary defining the roles. Roles: TypeAlias = dict[str, Union[str, Iterable[str]]] +#: Type alias for a simulation dictionary with undated variables. +UndatedVariable: TypeAlias = dict[str, object] + +#: Type alias for a simulation dictionary with dated variables. +DatedVariable: TypeAlias = dict[str, UndatedVariable] + #: Type alias for a simulation dictionary with abbreviated entities. -Variables: TypeAlias = dict[str, dict[str, object]] +Variables: TypeAlias = dict[str, Union[UndatedVariable, DatedVariable]] #: Type alias for a simulation with fully specified single entities. SingleEntities: TypeAlias = dict[str, dict[str, Variables]] @@ -104,6 +110,9 @@ class Holder(Protocol[V]): def variable(self) -> Variable[T]: """Get the Variable of the Holder.""" + def get_array(self, __period: str) -> Array[T] | None: + """Get the values of the Variable for a given Period.""" + def set_input( self, __period: Period, From 94e719401bcb9fb90adc8af407bed412a6ce1cc8 Mon Sep 17 00:00:00 2001 From: Allan - CodeWorks Date: Mon, 8 Jan 2024 10:57:29 +0100 Subject: [PATCH 058/188] Edit CI workflow to authenticate to Pypi via API token --- .github/workflows/workflow.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index b38c2ce258..578b371cb3 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -184,8 +184,8 @@ jobs: needs: [ check-for-functional-changes ] if: needs.check-for-functional-changes.outputs.status == 'success' env: - PYPI_USERNAME: openfisca-bot - PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + PYPI_USERNAME: __token__ + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_OPENFISCA_BOT }} CIRCLE_TOKEN: ${{ secrets.CIRCLECI_V1_OPENFISCADOC_TOKEN }} # Personal API token created in CircleCI to grant full read and write permissions steps: @@ -213,7 +213,7 @@ jobs: key: release-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - name: Upload a Python package to PyPi - run: twine upload dist/* --username $PYPI_USERNAME --password $PYPI_PASSWORD + run: twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN - name: Publish a git tag run: "${GITHUB_WORKSPACE}/.github/publish-git-tag.sh" From e0bb414ea8c9cae7f057987a12e7ff1ea66aae2d Mon Sep 17 00:00:00 2001 From: Allan - CodeWorks Date: Mon, 8 Jan 2024 11:17:57 +0100 Subject: [PATCH 059/188] Updates CHANGELOG and setup.py for 41.4.2 --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99c4419bae..c013acf3f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.4.2 [#1203](https://github.com/openfisca/openfisca-core/pull/1203) + +#### Technical changes + +- Changes the Pypi's deployment authentication way to use token API following Pypi's 2FA enforcement starting 2024/01/01. + ### 41.4.1 [#1202](https://github.com/openfisca/openfisca-core/pull/1202) #### Technical changes diff --git a/setup.py b/setup.py index fc0bbfef75..b804cbb79f 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.4.1", + version="41.4.2", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From fbcb28f1bfd37c6b05658babe0b4c41082e66926 Mon Sep 17 00:00:00 2001 From: Thomas Guillet Date: Mon, 5 Feb 2024 15:47:22 +0100 Subject: [PATCH 060/188] Update test --- tests/core/test_tracers.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index d0e25f8667..033a83e76a 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -10,6 +10,7 @@ from openfisca_country_template.variables.housing import HousingOccupancyStatus from openfisca_core.simulations import CycleError, Simulation, SpiralError +from openfisca_core.periods import period from openfisca_core.tracers import ( FullTracer, SimpleTracer, @@ -28,13 +29,15 @@ class StubSimulation(Simulation): def __init__(self): self.exception = None self.max_spiral_loops = 1 + self.max_spiral_lookback_months = 24 + self.invalidated_cache_items = [] def _calculate(self, variable, period): if self.exception: raise self.exception def invalidate_cache_entry(self, variable, period): - pass + self.invalidated_cache_items.append((variable, period)) def purge_cache_of_invalid_values(self): pass @@ -120,13 +123,18 @@ def test_cycle_error(tracer): def test_spiral_error(tracer): simulation = StubSimulation() simulation.tracer = tracer - tracer.record_calculation_start("a", 2017) - tracer.record_calculation_start("a", 2016) - tracer.record_calculation_start("a", 2015) + tracer.record_calculation_start("a", period(2017)) + tracer.record_calculation_start("b", period(2016)) + tracer.record_calculation_start("a", period(2016)) + tracer.record_calculation_start("b", period(2015)) + tracer.record_calculation_start("a", period(2015)) with raises(SpiralError): simulation._check_for_cycle("a", 2015) + assert len(simulation.invalidated_cache_items) == 3 + assert len(tracer.stack) == 5 + def test_full_tracer_one_calculation(tracer): tracer._enter_calculation("a", 2017) From 7c6eba3f8530e02645d19a247b32c4a74feb9379 Mon Sep 17 00:00:00 2001 From: Thomas Guillet Date: Mon, 5 Feb 2024 11:14:16 +0100 Subject: [PATCH 061/188] Simplify cache invalidation as spiral error occur --- openfisca_core/simulations/simulation.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 9e7e0034ea..1b3ebbf0b7 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -412,17 +412,12 @@ def invalidate_cache_entry(self, variable: str, period): self.invalidated_caches.add(Cache(variable, period)) def invalidate_spiral_variables(self, variable: str): - # Visit the stack, from the bottom (most recent) up; we know that we'll find - # the variable implicated in the spiral (max_spiral_loops+1) times; we keep the - # intermediate values computed (to avoid impacting performance) but we mark them - # for deletion from the cache once the calculation ends. - count = 0 - for frame in reversed(self.tracer.stack): - self.invalidate_cache_entry(str(frame["name"]), frame["period"]) - if frame["name"] == variable: - count += 1 - if count > self.max_spiral_loops: - break + invalidate_entries = False + for frame in self.tracer.stack: + if invalidate_entries: + self.invalidate_cache_entry(str(frame["name"]), frame["period"]) + elif frame["name"] == variable: + invalidate_entries = True # ----- Methods to access stored values ----- # From 32a5b426443b1bb72df083a75000a823c4f8746a Mon Sep 17 00:00:00 2001 From: Thomas Guillet Date: Mon, 5 Feb 2024 16:55:16 +0100 Subject: [PATCH 062/188] Explicit refactor with a comment --- openfisca_core/simulations/simulation.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 1b3ebbf0b7..6c55d974dc 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -412,12 +412,19 @@ def invalidate_cache_entry(self, variable: str, period): self.invalidated_caches.add(Cache(variable, period)) def invalidate_spiral_variables(self, variable: str): + initial_call_found = False invalidate_entries = False + # 1. find the initial variable call + # 2. find the next variable call + # 3. invalidate all frame items from there for frame in self.tracer.stack: - if invalidate_entries: - self.invalidate_cache_entry(str(frame["name"]), frame["period"]) + if initial_call_found: + if not invalidate_entries and frame["name"] == variable: + invalidate_entries = True + if invalidate_entries: + self.invalidate_cache_entry(str(frame["name"]), frame["period"]) elif frame["name"] == variable: - invalidate_entries = True + initial_call_found = True # ----- Methods to access stored values ----- # From f1dc1906366ecea24697af0c864e627dad4e8e3e Mon Sep 17 00:00:00 2001 From: Thomas Guillet Date: Tue, 6 Feb 2024 15:20:37 +0100 Subject: [PATCH 063/188] Reduce spiral test --- openfisca_core/simulations/simulation.py | 3 +++ tests/core/test_cycles.py | 6 ++++-- tests/core/test_tracers.py | 5 +---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 6c55d974dc..197204d948 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -409,9 +409,12 @@ def _check_for_cycle(self, variable: str, period): raise errors.SpiralError(message, variable) def invalidate_cache_entry(self, variable: str, period): + print((variable, period)) self.invalidated_caches.add(Cache(variable, period)) def invalidate_spiral_variables(self, variable: str): + print((variable)) + print(self.tracer.stack) initial_call_found = False invalidate_entries = False # 1. find the initial variable call diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index 14886532c6..a2b034ec8a 100644 --- a/tests/core/test_cycles.py +++ b/tests/core/test_cycles.py @@ -128,12 +128,14 @@ def test_spirals_result_in_default_value(simulation, reference_period): def test_spiral_heuristic(simulation, reference_period): variable5 = simulation.calculate("variable5", period=reference_period) + tools.assert_near(variable5, [11]) + variable6 = simulation.calculate("variable6", period=reference_period) + tools.assert_near(variable6, [11]) + variable6_last_month = simulation.calculate( "variable6", reference_period.last_month ) - tools.assert_near(variable5, [11]) - tools.assert_near(variable6, [11]) tools.assert_near(variable6_last_month, [11]) diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index 033a83e76a..e320278254 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -29,7 +29,6 @@ class StubSimulation(Simulation): def __init__(self): self.exception = None self.max_spiral_loops = 1 - self.max_spiral_lookback_months = 24 self.invalidated_cache_items = [] def _calculate(self, variable, period): @@ -126,11 +125,9 @@ def test_spiral_error(tracer): tracer.record_calculation_start("a", period(2017)) tracer.record_calculation_start("b", period(2016)) tracer.record_calculation_start("a", period(2016)) - tracer.record_calculation_start("b", period(2015)) - tracer.record_calculation_start("a", period(2015)) with raises(SpiralError): - simulation._check_for_cycle("a", 2015) + simulation._check_for_cycle("a", 2016) assert len(simulation.invalidated_cache_items) == 3 assert len(tracer.stack) == 5 From a20efed33084e73411b6512b4f29b194437fa8f6 Mon Sep 17 00:00:00 2001 From: Thomas Guillet Date: Tue, 6 Feb 2024 15:35:53 +0100 Subject: [PATCH 064/188] Improve spiral and cycle tests --- openfisca_core/simulations/simulation.py | 27 ++++++++++-------------- tests/core/test_cycles.py | 6 ++---- tests/core/test_tracers.py | 4 ++-- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 197204d948..9e7e0034ea 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -409,25 +409,20 @@ def _check_for_cycle(self, variable: str, period): raise errors.SpiralError(message, variable) def invalidate_cache_entry(self, variable: str, period): - print((variable, period)) self.invalidated_caches.add(Cache(variable, period)) def invalidate_spiral_variables(self, variable: str): - print((variable)) - print(self.tracer.stack) - initial_call_found = False - invalidate_entries = False - # 1. find the initial variable call - # 2. find the next variable call - # 3. invalidate all frame items from there - for frame in self.tracer.stack: - if initial_call_found: - if not invalidate_entries and frame["name"] == variable: - invalidate_entries = True - if invalidate_entries: - self.invalidate_cache_entry(str(frame["name"]), frame["period"]) - elif frame["name"] == variable: - initial_call_found = True + # Visit the stack, from the bottom (most recent) up; we know that we'll find + # the variable implicated in the spiral (max_spiral_loops+1) times; we keep the + # intermediate values computed (to avoid impacting performance) but we mark them + # for deletion from the cache once the calculation ends. + count = 0 + for frame in reversed(self.tracer.stack): + self.invalidate_cache_entry(str(frame["name"]), frame["period"]) + if frame["name"] == variable: + count += 1 + if count > self.max_spiral_loops: + break # ----- Methods to access stored values ----- # diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index a2b034ec8a..14886532c6 100644 --- a/tests/core/test_cycles.py +++ b/tests/core/test_cycles.py @@ -128,14 +128,12 @@ def test_spirals_result_in_default_value(simulation, reference_period): def test_spiral_heuristic(simulation, reference_period): variable5 = simulation.calculate("variable5", period=reference_period) - tools.assert_near(variable5, [11]) - variable6 = simulation.calculate("variable6", period=reference_period) - tools.assert_near(variable6, [11]) - variable6_last_month = simulation.calculate( "variable6", reference_period.last_month ) + tools.assert_near(variable5, [11]) + tools.assert_near(variable6, [11]) tools.assert_near(variable6_last_month, [11]) diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index e320278254..677fccdd97 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -127,10 +127,10 @@ def test_spiral_error(tracer): tracer.record_calculation_start("a", period(2016)) with raises(SpiralError): - simulation._check_for_cycle("a", 2016) + simulation._check_for_cycle("a", period(2016)) assert len(simulation.invalidated_cache_items) == 3 - assert len(tracer.stack) == 5 + assert len(tracer.stack) == 3 def test_full_tracer_one_calculation(tracer): From c2d8fb446d385cd6d24877766ba8d08c462169c7 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 14:09:57 +0100 Subject: [PATCH 065/188] test: fix tracers' types --- openfisca_core/tracers/full_tracer.py | 2 +- openfisca_core/tracers/simple_tracer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openfisca_core/tracers/full_tracer.py b/openfisca_core/tracers/full_tracer.py index ee127792b3..085607e125 100644 --- a/openfisca_core/tracers/full_tracer.py +++ b/openfisca_core/tracers/full_tracer.py @@ -28,7 +28,7 @@ def __init__(self) -> None: def record_calculation_start( self, variable: str, - period: Period, + period: Period | int, ) -> None: self._simple_tracer.record_calculation_start(variable, period) self._enter_calculation(variable, period) diff --git a/openfisca_core/tracers/simple_tracer.py b/openfisca_core/tracers/simple_tracer.py index 1d56453153..27bcad2e8c 100644 --- a/openfisca_core/tracers/simple_tracer.py +++ b/openfisca_core/tracers/simple_tracer.py @@ -17,7 +17,7 @@ class SimpleTracer: def __init__(self) -> None: self._stack = [] - def record_calculation_start(self, variable: str, period: Period) -> None: + def record_calculation_start(self, variable: str, period: Period | int) -> None: self.stack.append({"name": variable, "period": period}) def record_calculation_result(self, value: ArrayLike) -> None: From 399928b14eab51faac3aae559cd02ac2f6080525 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 14:10:33 +0100 Subject: [PATCH 066/188] test: make assertion style (aaa) uniform --- tests/core/test_tracers.py | 63 +++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index 677fccdd97..6cb0575f65 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -9,8 +9,8 @@ from openfisca_country_template.variables.housing import HousingOccupancyStatus +from openfisca_core import periods from openfisca_core.simulations import CycleError, Simulation, SpiralError -from openfisca_core.periods import period from openfisca_core.tracers import ( FullTracer, SimpleTracer, @@ -61,10 +61,12 @@ def tracer(): @mark.parametrize("tracer", [SimpleTracer(), FullTracer()]) def test_stack_one_level(tracer): tracer.record_calculation_start("a", 2017) + assert len(tracer.stack) == 1 assert tracer.stack == [{"name": "a", "period": 2017}] tracer.record_calculation_end() + assert tracer.stack == [] @@ -72,6 +74,7 @@ def test_stack_one_level(tracer): def test_stack_two_levels(tracer): tracer.record_calculation_start("a", 2017) tracer.record_calculation_start("b", 2017) + assert len(tracer.stack) == 2 assert tracer.stack == [ {"name": "a", "period": 2017}, @@ -79,6 +82,7 @@ def test_stack_two_levels(tracer): ] tracer.record_calculation_end() + assert len(tracer.stack) == 1 assert tracer.stack == [{"name": "a", "period": 2017}] @@ -110,32 +114,43 @@ def test_exception_robustness(): def test_cycle_error(tracer): simulation = StubSimulation() simulation.tracer = tracer + tracer.record_calculation_start("a", 2017) - simulation._check_for_cycle("a", 2017) + + assert not simulation._check_for_cycle("a", 2017) tracer.record_calculation_start("a", 2017) + with raises(CycleError): simulation._check_for_cycle("a", 2017) + assert len(tracer.stack) == 2 + assert tracer.stack == [ + {"name": "a", "period": 2017}, + {"name": "a", "period": 2017}, + ] + @mark.parametrize("tracer", [SimpleTracer(), FullTracer()]) def test_spiral_error(tracer): simulation = StubSimulation() simulation.tracer = tracer - tracer.record_calculation_start("a", period(2017)) - tracer.record_calculation_start("b", period(2016)) - tracer.record_calculation_start("a", period(2016)) + + tracer.record_calculation_start("a", periods.period(2017)) + tracer.record_calculation_start("b", periods.period(2016)) + tracer.record_calculation_start("a", periods.period(2016)) with raises(SpiralError): - simulation._check_for_cycle("a", period(2016)) + simulation._check_for_cycle("a", periods.period(2016)) - assert len(simulation.invalidated_cache_items) == 3 assert len(tracer.stack) == 3 + assert len(simulation.invalidated_cache_items) == 3 def test_full_tracer_one_calculation(tracer): tracer._enter_calculation("a", 2017) tracer._exit_calculation() + assert tracer.stack == [] assert len(tracer.trees) == 1 assert tracer.trees[0].name == "a" @@ -145,13 +160,10 @@ def test_full_tracer_one_calculation(tracer): def test_full_tracer_2_branches(tracer): tracer._enter_calculation("a", 2017) - tracer._enter_calculation("b", 2017) tracer._exit_calculation() - tracer._enter_calculation("c", 2017) tracer._exit_calculation() - tracer._exit_calculation() assert len(tracer.trees) == 1 @@ -161,7 +173,6 @@ def test_full_tracer_2_branches(tracer): def test_full_tracer_2_trees(tracer): tracer._enter_calculation("b", 2017) tracer._exit_calculation() - tracer._enter_calculation("c", 2017) tracer._exit_calculation() @@ -263,12 +274,13 @@ def test_calculation_time(): tracer._record_start_time(1500) tracer._record_end_time(2500) tracer._exit_calculation() - performance_json = tracer.performance_log._json() + assert performance_json["name"] == "All calculations" assert performance_json["value"] == 1000 simulation_children = performance_json["children"] + assert simulation_children[0]["name"] == "a<2019>" assert simulation_children[0]["value"] == 1000 @@ -331,11 +343,15 @@ def test_flat_trace_calc_time(tracer_calc_time): def test_generate_performance_table(tracer_calc_time, tmpdir): tracer = tracer_calc_time tracer.generate_performance_tables(tmpdir) + with open(os.path.join(tmpdir, "performance_table.csv"), "r") as csv_file: csv_reader = csv.DictReader(csv_file) csv_rows = list(csv_reader) + assert len(csv_rows) == 4 + a_row = next(row for row in csv_rows if row["name"] == "a<2019>") + assert float(a_row["calculation_time"]) == 1000 assert float(a_row["formula_time"]) == 190 @@ -344,8 +360,11 @@ def test_generate_performance_table(tracer_calc_time, tmpdir): ) as csv_file: aggregated_csv_reader = csv.DictReader(csv_file) aggregated_csv_rows = list(aggregated_csv_reader) + assert len(aggregated_csv_rows) == 3 + a_row = next(row for row in aggregated_csv_rows if row["name"] == "a") + assert float(a_row["calculation_time"]) == 1000 + 200 assert float(a_row["formula_time"]) == 190 + 200 @@ -397,8 +416,8 @@ def test_log_format(tracer): tracer._exit_calculation() tracer.record_calculation_result(numpy.asarray([2])) tracer._exit_calculation() - lines = tracer.computation_log.lines() + assert lines[0] == " A<2017> >> [2]" assert lines[1] == " B<2017> >> [1]" @@ -407,12 +426,11 @@ def test_log_format_forest(tracer): tracer._enter_calculation("A", 2017) tracer.record_calculation_result(numpy.asarray([1])) tracer._exit_calculation() - tracer._enter_calculation("B", 2017) tracer.record_calculation_result(numpy.asarray([2])) tracer._exit_calculation() - lines = tracer.computation_log.lines() + assert lines[0] == " A<2017> >> [1]" assert lines[1] == " B<2017> >> [2]" @@ -421,8 +439,8 @@ def test_log_aggregate(tracer): tracer._enter_calculation("A", 2017) tracer.record_calculation_result(numpy.asarray([1])) tracer._exit_calculation() - lines = tracer.computation_log.lines(aggregate=True) + assert lines[0] == " A<2017> >> {'avg': 1.0, 'max': 1, 'min': 1}" @@ -432,8 +450,8 @@ def test_log_aggregate_with_enum(tracer): HousingOccupancyStatus.encode(numpy.repeat("tenant", 100)) ) tracer._exit_calculation() - lines = tracer.computation_log.lines(aggregate=True) + assert ( lines[0] == " A<2017> >> {'avg': EnumArray(HousingOccupancyStatus.tenant), 'max': EnumArray(HousingOccupancyStatus.tenant), 'min': EnumArray(HousingOccupancyStatus.tenant)}" @@ -444,8 +462,8 @@ def test_log_aggregate_with_strings(tracer): tracer._enter_calculation("A", 2017) tracer.record_calculation_result(numpy.repeat("foo", 100)) tracer._exit_calculation() - lines = tracer.computation_log.lines(aggregate=True) + assert lines[0] == " A<2017> >> {'avg': '?', 'max': '?', 'min': '?'}" @@ -474,8 +492,8 @@ def test_no_wrapping(tracer): HousingOccupancyStatus.encode(numpy.repeat("tenant", 100)) ) tracer._exit_calculation() - lines = tracer.computation_log.lines() + assert "'tenant'" in lines[0] assert "\n" not in lines[0] @@ -486,8 +504,8 @@ def test_trace_enums(tracer): HousingOccupancyStatus.encode(numpy.array(["tenant"])) ) tracer._exit_calculation() - lines = tracer.computation_log.lines() + assert lines[0] == " A<2017> >> ['tenant']" @@ -499,9 +517,12 @@ def test_trace_enums(tracer): def check_tracing_params(accessor, param_key): tracer = FullTracer() + tracer._enter_calculation("A", "2015-01") + tracingParams = TracingParameterNodeAtInstant(parameters("2015-01-01"), tracer) param = accessor(tracingParams) + assert tracer.trees[0].parameters[0].name == param_key assert tracer.trees[0].parameters[0].value == approx(param) @@ -549,6 +570,6 @@ def test_browse_trace(): tracer._enter_calculation("F", 2017) tracer._exit_calculation() tracer._exit_calculation() - browsed_nodes = [node.name for node in tracer.browse_trace()] + assert browsed_nodes == ["B", "C", "D", "E", "F"] From aab5f35502e1ab7e0d5d183a5199d06facfa3fd6 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 15:02:00 +0100 Subject: [PATCH 067/188] test: move invalidate cache test to simulation --- tests/core/test_simulations.py | 17 +++++++++++++++++ tests/core/test_tracers.py | 9 ++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/core/test_simulations.py b/tests/core/test_simulations.py index d55e92673f..015820fe2f 100644 --- a/tests/core/test_simulations.py +++ b/tests/core/test_simulations.py @@ -1,6 +1,9 @@ +import pytest + from openfisca_country_template.situation_examples import single from openfisca_core.simulations import SimulationBuilder +from openfisca_core import periods, errors def test_calculate_full_tracer(tax_benefit_system): @@ -62,3 +65,17 @@ def test_get_memory_usage(tax_benefit_system): memory_usage = simulation.get_memory_usage(variables=["salary"]) assert memory_usage["total_nb_bytes"] > 0 assert len(memory_usage["by_variable"]) == 1 + + +def test_invalidate_cache_when_spiral_error_detected(tax_benefit_system): + simulation = SimulationBuilder().build_default_simulation(tax_benefit_system) + tracer = simulation.tracer + + tracer.record_calculation_start("a", periods.period(2017)) + tracer.record_calculation_start("b", periods.period(2016)) + tracer.record_calculation_start("a", periods.period(2016)) + + with pytest.raises(errors.SpiralError): + simulation._check_for_cycle("a", periods.period(2016)) + + assert len(simulation.invalidated_caches) == 3 diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index 6cb0575f65..41acf68bc4 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -29,14 +29,13 @@ class StubSimulation(Simulation): def __init__(self): self.exception = None self.max_spiral_loops = 1 - self.invalidated_cache_items = [] def _calculate(self, variable, period): if self.exception: raise self.exception def invalidate_cache_entry(self, variable, period): - self.invalidated_cache_items.append((variable, period)) + pass def purge_cache_of_invalid_values(self): pass @@ -144,7 +143,11 @@ def test_spiral_error(tracer): simulation._check_for_cycle("a", periods.period(2016)) assert len(tracer.stack) == 3 - assert len(simulation.invalidated_cache_items) == 3 + assert tracer.stack == [ + {"name": "a", "period": periods.period(2017)}, + {"name": "b", "period": periods.period(2016)}, + {"name": "a", "period": periods.period(2016)}, + ] def test_full_tracer_one_calculation(tracer): From 24ee6e92bcf0e122930919d7a8973a53224bf206 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 15:16:48 +0100 Subject: [PATCH 068/188] build: bump version --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- tests/core/test_simulations.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c013acf3f9..7834af7049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### 41.4.3 [#1206](https://github.com/openfisca/openfisca-core/pull/1206) + +#### Technical changes + +- Increase spiral and cycle tests robustness. + - The current test is ambiguous, as it hides a failure at the first spiral + occurrence (from 2017 to 2016). + ### 41.4.2 [#1203](https://github.com/openfisca/openfisca-core/pull/1203) #### Technical changes diff --git a/setup.py b/setup.py index b804cbb79f..192fb3a9f7 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.4.2", + version="41.4.3", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ diff --git a/tests/core/test_simulations.py b/tests/core/test_simulations.py index 015820fe2f..18050b6bc5 100644 --- a/tests/core/test_simulations.py +++ b/tests/core/test_simulations.py @@ -2,8 +2,8 @@ from openfisca_country_template.situation_examples import single +from openfisca_core import errors, periods from openfisca_core.simulations import SimulationBuilder -from openfisca_core import periods, errors def test_calculate_full_tracer(tax_benefit_system): From cc99e33ea0503a800731a2a2e0209754927056f5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 18:34:11 +0100 Subject: [PATCH 069/188] dist: use package version with repo url --- openfisca_tasks/test_code.mk | 2 +- .../__init__.py => fixtures/extensions.py} | 0 tests/web_api/__init__.py | 4 ---- tests/web_api/loader/__init__.py | 0 4 files changed, 1 insertion(+), 5 deletions(-) rename tests/{web_api/case_with_extension/__init__.py => fixtures/extensions.py} (100%) delete mode 100644 tests/web_api/loader/__init__.py diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index 2734d20275..4f9f97fab0 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -11,7 +11,7 @@ install: install-deps install-edit install-test install-test: @$(call print_help,$@:) @pip install --upgrade --no-dependencies openfisca-country-template - @pip install --upgrade --no-dependencies openfisca-extension-template + @pip install git+https://github.com/openfisca/country-template@training-upgrade-fix ## Run openfisca-core & country/extension template tests. test-code: test-core test-country test-extension diff --git a/tests/web_api/case_with_extension/__init__.py b/tests/fixtures/extensions.py similarity index 100% rename from tests/web_api/case_with_extension/__init__.py rename to tests/fixtures/extensions.py diff --git a/tests/web_api/__init__.py b/tests/web_api/__init__.py index 88d4796eba..e69de29bb2 100644 --- a/tests/web_api/__init__.py +++ b/tests/web_api/__init__.py @@ -1,4 +0,0 @@ -import pkg_resources - -TEST_COUNTRY_PACKAGE_NAME = "openfisca_country_template" -distribution = pkg_resources.get_distribution(TEST_COUNTRY_PACKAGE_NAME) diff --git a/tests/web_api/loader/__init__.py b/tests/web_api/loader/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 5ec42d4c6b86c306f6150e82c465fcff0d712970 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 18:35:31 +0100 Subject: [PATCH 070/188] refactor: use proper metadata interface --- .../taxbenefitsystems/tax_benefit_system.py | 19 ++++++++++++------- setup.py | 1 - 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index f03ecb0571..f847d788b3 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -9,6 +9,8 @@ import functools import glob import importlib +import importlib.metadata +import importlib.util import inspect import linecache import logging @@ -16,8 +18,6 @@ import sys import traceback -import importlib_metadata - from openfisca_core import commons, periods, variables from openfisca_core.entities import Entity from openfisca_core.errors import VariableNameConflictError, VariableNotFoundError @@ -524,9 +524,9 @@ def get_package_metadata(self) -> Dict[str, str]: package_name = module.__package__.split(".")[0] try: - distribution = importlib_metadata.distribution(package_name) + distribution = importlib.metadata.distribution(package_name) - except importlib_metadata.PackageNotFoundError: + except importlib.metadata.PackageNotFoundError: return fallback_metadata source_file = inspect.getsourcefile(module) @@ -540,9 +540,14 @@ def get_package_metadata(self) -> Dict[str, str]: metadata = distribution.metadata return { - "name": metadata["Name"].lower(), - "version": distribution.version, - "repository_url": metadata["Home-page"], + "name": metadata.get("Name").lower(), + "version": metadata.get("version"), + "repository_url": next( + filter( + lambda url: url.startswith("Repository"), + metadata.get_all("Project-URL"), + ) + ).split("Repository, ")[-1], "location": location, } diff --git a/setup.py b/setup.py index 192fb3a9f7..bc79378ac7 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,6 @@ general_requirements = [ "PyYAML >=6.0, <7.0", "dpath >=2.1.4, <3.0", - "importlib-metadata >=6.1.0, <7.0", "numexpr >=2.8.4, <3.0", "numpy >=1.24.2, <1.25", "pendulum >=2.1.2, <3.0.0", From 54c91ab5d48254fe94bf95002687500cdbe0343c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 18:36:22 +0100 Subject: [PATCH 071/188] test: add distribution fixture --- conftest.py | 1 + tests/fixtures/extensions.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/conftest.py b/conftest.py index 569859338d..fbe03e7d37 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,7 @@ pytest_plugins = [ "tests.fixtures.appclient", "tests.fixtures.entities", + "tests.fixtures.extensions", "tests.fixtures.simulations", "tests.fixtures.taxbenefitsystems", ] diff --git a/tests/fixtures/extensions.py b/tests/fixtures/extensions.py index e69de29bb2..631dcbc0d7 100644 --- a/tests/fixtures/extensions.py +++ b/tests/fixtures/extensions.py @@ -0,0 +1,18 @@ +from importlib import metadata + +import pytest + + +@pytest.fixture() +def test_country_package_name(): + return "openfisca_country_template" + + +@pytest.fixture() +def test_extension_package_name(): + return "openfisca_extension_template" + + +@pytest.fixture() +def distribution(test_country_package_name): + return metadata.distribution(test_country_package_name) From 85ff2a7ce8407af2bbb5eb0730b74ac103add962 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 18:41:03 +0100 Subject: [PATCH 072/188] test: fix web-api headers test --- .../taxbenefitsystems/tax_benefit_system.py | 2 +- openfisca_web_api/loader/__init__.py | 5 +++-- tests/web_api/test_headers.py | 18 ++++++------------ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index f847d788b3..116f1c5db3 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -541,7 +541,7 @@ def get_package_metadata(self) -> Dict[str, str]: return { "name": metadata.get("Name").lower(), - "version": metadata.get("version"), + "version": metadata.get("Version"), "repository_url": next( filter( lambda url: url.startswith("Repository"), diff --git a/openfisca_web_api/loader/__init__.py b/openfisca_web_api/loader/__init__.py index c62831c36d..fcea068e21 100644 --- a/openfisca_web_api/loader/__init__.py +++ b/openfisca_web_api/loader/__init__.py @@ -11,13 +11,14 @@ def build_data(tax_benefit_system): country_package_metadata = tax_benefit_system.get_package_metadata() parameters = build_parameters(tax_benefit_system, country_package_metadata) variables = build_variables(tax_benefit_system, country_package_metadata) + entities = build_entities(tax_benefit_system) data = { "tax_benefit_system": tax_benefit_system, - "country_package_metadata": tax_benefit_system.get_package_metadata(), + "country_package_metadata": country_package_metadata, "openAPI_spec": None, "parameters": parameters, "variables": variables, - "entities": build_entities(tax_benefit_system), + "entities": entities, } data["openAPI_spec"] = build_openAPI_specification(data) diff --git a/tests/web_api/test_headers.py b/tests/web_api/test_headers.py index 65c0623c8d..c5464d91b1 100644 --- a/tests/web_api/test_headers.py +++ b/tests/web_api/test_headers.py @@ -1,16 +1,10 @@ -# -*- coding: utf-8 -*- - -from . import distribution - - -def test_package_name_header(test_client): +def test_package_name_header(test_client, distribution): + name = distribution.metadata.get("Name").lower() parameters_response = test_client.get("/parameters") - assert parameters_response.headers.get("Country-Package") == distribution.key + assert parameters_response.headers.get("Country-Package") == name -def test_package_version_header(test_client): +def test_package_version_header(test_client, distribution): + version = distribution.metadata.get("Version") parameters_response = test_client.get("/parameters") - assert ( - parameters_response.headers.get("Country-Package-Version") - == distribution.version - ) + assert parameters_response.headers.get("Country-Package-Version") == version From 9e0fab76d490963808699c2f21f03c9ddf248783 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 18:42:38 +0100 Subject: [PATCH 073/188] test: fix web-api case tests --- .../case_with_extension/test_extensions.py | 33 +++++++++++-------- .../web_api/case_with_reform/test_reforms.py | 24 +++++++------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/tests/web_api/case_with_extension/test_extensions.py b/tests/web_api/case_with_extension/test_extensions.py index 7984025196..be5ee6bf24 100644 --- a/tests/web_api/case_with_extension/test_extensions.py +++ b/tests/web_api/case_with_extension/test_extensions.py @@ -1,34 +1,39 @@ -# -*- coding: utf-8 -*- +from http import client -from http.client import OK +import pytest from openfisca_core.scripts import build_tax_benefit_system from openfisca_web_api.app import create_app -TEST_COUNTRY_PACKAGE_NAME = "openfisca_country_template" -TEST_EXTENSION_PACKAGE_NAMES = ["openfisca_extension_template"] -tax_benefit_system = build_tax_benefit_system( - TEST_COUNTRY_PACKAGE_NAME, extensions=TEST_EXTENSION_PACKAGE_NAMES, reforms=None -) +@pytest.fixture() +def tax_benefit_system(test_country_package_name, test_extension_package_name): + return build_tax_benefit_system( + test_country_package_name, + extensions=[test_extension_package_name], + reforms=None, + ) + -extended_subject = create_app(tax_benefit_system).test_client() +@pytest.fixture() +def extended_subject(tax_benefit_system): + return create_app(tax_benefit_system).test_client() -def test_return_code(): +def test_return_code(extended_subject): parameters_response = extended_subject.get("/parameters") - assert parameters_response.status_code == OK + assert parameters_response.status_code == client.OK -def test_return_code_existing_parameter(): +def test_return_code_existing_parameter(extended_subject): extension_parameter_response = extended_subject.get( "/parameter/local_town.child_allowance.amount" ) - assert extension_parameter_response.status_code == OK + assert extension_parameter_response.status_code == client.OK -def test_return_code_existing_variable(): +def test_return_code_existing_variable(extended_subject): extension_variable_response = extended_subject.get( "/variable/local_town_child_allowance" ) - assert extension_variable_response.status_code == OK + assert extension_variable_response.status_code == client.OK diff --git a/tests/web_api/case_with_reform/test_reforms.py b/tests/web_api/case_with_reform/test_reforms.py index 993dceda45..afcb811443 100644 --- a/tests/web_api/case_with_reform/test_reforms.py +++ b/tests/web_api/case_with_reform/test_reforms.py @@ -5,23 +5,25 @@ from openfisca_core import scripts from openfisca_web_api import app -TEST_COUNTRY_PACKAGE_NAME = "openfisca_country_template" -TEST_REFORMS_PATHS = [ - f"{TEST_COUNTRY_PACKAGE_NAME}.reforms.add_dynamic_variable.add_dynamic_variable", - f"{TEST_COUNTRY_PACKAGE_NAME}.reforms.add_new_tax.add_new_tax", - f"{TEST_COUNTRY_PACKAGE_NAME}.reforms.flat_social_security_contribution.flat_social_security_contribution", - f"{TEST_COUNTRY_PACKAGE_NAME}.reforms.modify_social_security_taxation.modify_social_security_taxation", - f"{TEST_COUNTRY_PACKAGE_NAME}.reforms.removal_basic_income.removal_basic_income", -] + +@pytest.fixture() +def test_reforms_path(test_country_package_name): + return [ + f"{test_country_package_name}.reforms.add_dynamic_variable.add_dynamic_variable", + f"{test_country_package_name}.reforms.add_new_tax.add_new_tax", + f"{test_country_package_name}.reforms.flat_social_security_contribution.flat_social_security_contribution", + f"{test_country_package_name}.reforms.modify_social_security_taxation.modify_social_security_taxation", + f"{test_country_package_name}.reforms.removal_basic_income.removal_basic_income", + ] # Create app as in 'openfisca serve' script @pytest.fixture -def client(): +def client(test_country_package_name, test_reforms_path): tax_benefit_system = scripts.build_tax_benefit_system( - TEST_COUNTRY_PACKAGE_NAME, + test_country_package_name, extensions=None, - reforms=TEST_REFORMS_PATHS, + reforms=test_reforms_path, ) return app.create_app(tax_benefit_system).test_client() From b056c2c70e49b1113645a88d0681772fcb76a625 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 18 Mar 2024 19:11:53 +0100 Subject: [PATCH 074/188] chore: remove test leftover --- openfisca_tasks/test_code.mk | 2 +- tests/web_api/basic_case/__init__.py | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 tests/web_api/basic_case/__init__.py diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index 4f9f97fab0..e98611f38c 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -10,8 +10,8 @@ install: install-deps install-edit install-test ## Enable regression testing with template repositories. install-test: @$(call print_help,$@:) - @pip install --upgrade --no-dependencies openfisca-country-template @pip install git+https://github.com/openfisca/country-template@training-upgrade-fix + @pip install --upgrade --no-dependencies openfisca-extension-template ## Run openfisca-core & country/extension template tests. test-code: test-core test-country test-extension diff --git a/tests/web_api/basic_case/__init__.py b/tests/web_api/basic_case/__init__.py deleted file mode 100644 index bb39b2df14..0000000000 --- a/tests/web_api/basic_case/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -# -*- coding: utf-8 -*- -import pkg_resources - -from openfisca_core.scripts import build_tax_benefit_system -from openfisca_web_api.app import create_app - -TEST_COUNTRY_PACKAGE_NAME = "openfisca_country_template" -distribution = pkg_resources.get_distribution(TEST_COUNTRY_PACKAGE_NAME) -tax_benefit_system = build_tax_benefit_system( - TEST_COUNTRY_PACKAGE_NAME, extensions=None, reforms=None -) -subject = create_app(tax_benefit_system).test_client() From 53c50934b16af16f05b39db3497a6e855902d5e4 Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Thu, 2 May 2024 10:02:39 +0200 Subject: [PATCH 075/188] Use published Country Template --- openfisca_tasks/test_code.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index e98611f38c..2734d20275 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -10,7 +10,7 @@ install: install-deps install-edit install-test ## Enable regression testing with template repositories. install-test: @$(call print_help,$@:) - @pip install git+https://github.com/openfisca/country-template@training-upgrade-fix + @pip install --upgrade --no-dependencies openfisca-country-template @pip install --upgrade --no-dependencies openfisca-extension-template ## Run openfisca-core & country/extension template tests. From dd2683ad22c6a0780e22492b28bc412e331582cf Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Thu, 2 May 2024 10:32:12 +0200 Subject: [PATCH 076/188] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7834af7049..654a23fc70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.4.4 [#1208](https://github.com/openfisca/openfisca-core/pull/1208) + +#### Technical changes + +- Adapt testing pipeline to Country Template [v7](https://github.com/openfisca/country-template/pull/139). + ### 41.4.3 [#1206](https://github.com/openfisca/openfisca-core/pull/1206) #### Technical changes From c837aef035755e70224327f4376b6c894667713f Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Thu, 2 May 2024 10:37:52 +0200 Subject: [PATCH 077/188] Bump version number --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bc79378ac7..4594cd99f0 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.4.3", + version="41.4.4", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 1ae4e05cdef7efd2beb85c65808fa27da8c50d29 Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Sun, 5 May 2024 21:44:38 +0200 Subject: [PATCH 078/188] Increase metadata loading compatibility Bring back setup.py compatibility from before v41.4.4 while maintaining compatibility with pyproject.toml Increase resilience to missing metadata Simplify code by grouping exception catching Log warnings when metadata cannot be obtained --- .../taxbenefitsystems/tax_benefit_system.py | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 116f1c5db3..32193e15cf 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -506,48 +506,43 @@ def get_package_metadata(self) -> Dict[str, str]: if self.baseline: return self.baseline.get_package_metadata() - fallback_metadata = { - "name": self.__class__.__name__, - "version": "", - "repository_url": "", - "location": "", - } - module = inspect.getmodule(self) - if module is None: - return fallback_metadata - - if module.__package__ is None: - return fallback_metadata - - package_name = module.__package__.split(".")[0] - try: + source_file = inspect.getsourcefile(module) + package_name = module.__package__.split(".")[0] distribution = importlib.metadata.distribution(package_name) + source_metadata = distribution.metadata + except Exception as e: + log.warn("Unable to load package metadata, exposing default metadata", e) + source_metadata = { + "Name": self.__class__.__name__, + "Version": "", + "Home-page": "", + } - except importlib.metadata.PackageNotFoundError: - return fallback_metadata - - source_file = inspect.getsourcefile(module) - - if source_file is not None: + try: + source_file = inspect.getsourcefile(module) location = source_file.split(package_name)[0].rstrip("/") - - else: + except Exception as e: + log.warn("Unable to load package source folder", e) location = "" - metadata = distribution.metadata - - return { - "name": metadata.get("Name").lower(), - "version": metadata.get("Version"), - "repository_url": next( + repository_url = "" + if source_metadata.get("Project-URL"): # pyproject.toml metadata format + repository_url = next( filter( lambda url: url.startswith("Repository"), - metadata.get_all("Project-URL"), + source_metadata.get_all("Project-URL"), ) - ).split("Repository, ")[-1], + ).split("Repository, ")[-1] + else: # setup.py format + repository_url = source_metadata.get("Home-page") + + return { + "name": source_metadata.get("Name").lower(), + "version": source_metadata.get("Version"), + "repository_url": repository_url, "location": location, } From 6a92272d090597642aee32f38f37e7b644ee7c8e Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Sun, 5 May 2024 21:45:21 +0200 Subject: [PATCH 079/188] Set properly formatted fallback metadata values --- openfisca_core/taxbenefitsystems/tax_benefit_system.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 32193e15cf..54a7413d1a 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -517,8 +517,8 @@ def get_package_metadata(self) -> Dict[str, str]: log.warn("Unable to load package metadata, exposing default metadata", e) source_metadata = { "Name": self.__class__.__name__, - "Version": "", - "Home-page": "", + "Version": "0.0.0", + "Home-page": "https://openfisca.org", } try: @@ -526,7 +526,7 @@ def get_package_metadata(self) -> Dict[str, str]: location = source_file.split(package_name)[0].rstrip("/") except Exception as e: log.warn("Unable to load package source folder", e) - location = "" + location = "_unknown_" repository_url = "" if source_metadata.get("Project-URL"): # pyproject.toml metadata format From da03f046f59802b802f8ed75f672493f25ad7083 Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Sun, 5 May 2024 21:47:37 +0200 Subject: [PATCH 080/188] Bump version number and changelog --- CHANGELOG.md | 10 +++++++++- setup.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 654a23fc70..695ca11bc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog -### 41.4.4 [#1208](https://github.com/openfisca/openfisca-core/pull/1208) +### 41.4.5 [#1209](https://github.com/openfisca/openfisca-core/pull/1209) + +#### Technical changes + +- Support loading metadata from both `setup.py` and `pyproject.toml` package description files. + +### ~41.4.4~ [#1208](https://github.com/openfisca/openfisca-core/pull/1208) + +_Unpublished due to introduced backwards incompatibilities._ #### Technical changes diff --git a/setup.py b/setup.py index 4594cd99f0..bcedf5677d 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.4.4", + version="41.4.5", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 2091cd693bc03f6a71ecf7685df2be12172c7902 Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Tue, 21 May 2024 06:26:21 +0200 Subject: [PATCH 081/188] Revert OpenAPI version number to 3.0.0 This was bumped in 8be2ec4e7b598c70111a30458ebe6d8e40d96abc but we do not actually abide by 3.1.0, as can be seen in the use of `example` instead of `examples. v3.1.0 is a breaking change, the technical committee decided not to follow semver, see . Support for OpenAPI v3.1 is not offered by the current Legislation Explorer version and upgrading is too costly. See . --- openfisca_web_api/openAPI.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openfisca_web_api/openAPI.yml b/openfisca_web_api/openAPI.yml index 5eb1d8cf5c..ce935e5596 100644 --- a/openfisca_web_api/openAPI.yml +++ b/openfisca_web_api/openAPI.yml @@ -1,4 +1,4 @@ -openapi: "3.1.0" +openapi: "3.0.0" info: title: "{COUNTRY_PACKAGE_NAME} Web API" @@ -137,8 +137,8 @@ components: type: "object" additionalProperties: $ref: "#/components/schemas/Value" - propertyNames: # this keyword is part of JSON Schema but is not supported in OpenAPI Specification at the time of writing, see https://swagger.io/docs/specification/data-models/keywords/#unsupported - pattern: "^[12][0-9]{3}-[01][0-9]-[0-3][0-9]$" # all keys are ISO dates + # propertyNames: # this keyword is part of JSON Schema but is not supported in OpenAPI v3.0.0 + # pattern: "^[12][0-9]{3}-[01][0-9]-[0-3][0-9]$" # all keys are ISO dates Value: oneOf: From de1b0fde9ccf932684b7f93d11da3dff5114fab4 Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Tue, 21 May 2024 06:59:57 +0200 Subject: [PATCH 082/188] Test OpenAPI v3.0 compliance --- setup.py | 2 +- tests/web_api/test_spec.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index bcedf5677d..fa6715c797 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ "idna >=3.4, < 4.0", "isort >=5.12.0, < 6.0", "mypy >=1.1.1, < 2.0", - "openapi-spec-validator >=0.5.6, < 0.6.0", + "openapi-spec-validator >=0.7.1, < 0.8.0", "pycodestyle >=2.10.0, < 3.0", "pylint >=2.17.1, < 3.0", "xdoctest >=1.1.1, < 2.0", diff --git a/tests/web_api/test_spec.py b/tests/web_api/test_spec.py index 89f221ee0e..228cf27eb8 100644 --- a/tests/web_api/test_spec.py +++ b/tests/web_api/test_spec.py @@ -3,7 +3,7 @@ import dpath.util import pytest -from openapi_spec_validator import openapi_v3_spec_validator +from openapi_spec_validator import OpenAPIV30SpecValidator def assert_items_equal(x, y): @@ -62,4 +62,4 @@ def test_situation_definition(body): def test_respects_spec(body): - assert not [error for error in openapi_v3_spec_validator.iter_errors(body)] + assert not [error for error in OpenAPIV30SpecValidator(body).iter_errors()] From 1c655501c78763820b8952df3ff92c9f03d80c5d Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Tue, 21 May 2024 06:51:22 +0200 Subject: [PATCH 083/188] Bump version number --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 695ca11bc9..cf8f7d51de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 41.4.6 [#1210](https://github.com/openfisca/openfisca-core/pull/1210) + +#### Technical changes + +- Abide by OpenAPI v3.0.0 instead of v3.1.0 + - Drop support for `propertyNames` in `Values` definition + ### 41.4.5 [#1209](https://github.com/openfisca/openfisca-core/pull/1209) #### Technical changes diff --git a/setup.py b/setup.py index fa6715c797..cb41e7ec88 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.4.5", + version="41.4.6", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 77d18697cf29b1b273d258afc04fa8dbed4f1f89 Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Tue, 21 May 2024 06:07:21 +0200 Subject: [PATCH 084/188] Update doc building workflow to latest version Following https://github.com/openfisca/openfisca-doc/pull/308 --- .github/workflows/workflow.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 578b371cb3..727a25e8c7 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -186,7 +186,6 @@ jobs: env: PYPI_USERNAME: __token__ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_OPENFISCA_BOT }} - CIRCLE_TOKEN: ${{ secrets.CIRCLECI_V1_OPENFISCADOC_TOKEN }} # Personal API token created in CircleCI to grant full read and write permissions steps: - uses: actions/checkout@v2 @@ -220,7 +219,13 @@ jobs: - name: Update doc run: | - curl -X POST --header "Content-Type: application/json" -d '{"branch":"master"}' https://circleci.com/api/v1.1/project/github/openfisca/openfisca-doc/build?circle-token=${{ secrets.CIRCLECI_V1_OPENFISCADOC_TOKEN }} + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ + -d '{"ref":"master"}' build-conda: runs-on: ubuntu-22.04 From 345bb0bab3561cd868f0ad76025c52c070647951 Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Tue, 21 May 2024 06:16:50 +0200 Subject: [PATCH 085/188] [TOREMOVE] Test doc workflow dispatch --- .github/workflows/workflow.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 727a25e8c7..febc7bc49d 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -179,6 +179,19 @@ jobs: - id: stop-early run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi # The `check-for-functional-changes` job should always succeed regardless of the `has-functional-changes` script's exit code. Consequently, we do not use that exit code to trigger deploy, but rather a dedicated output variable `status`, to avoid a job failure if the exit code is different from 0. Conversely, if the job fails the entire workflow would be marked as `failed` which is disturbing for contributors. + deploy-doc: # TESTING + runs-on: ubuntu-latest + steps: + - name: Update doc + run: | + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ + -d '{"ref":"cd"}' + deploy: runs-on: ubuntu-22.04 needs: [ check-for-functional-changes ] From 34b5af3ac791a2e9ef403b0d9c1d0ca7f6070fac Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Tue, 21 May 2024 14:55:51 +0200 Subject: [PATCH 086/188] Remove test code --- .github/workflows/workflow.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index febc7bc49d..727a25e8c7 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -179,19 +179,6 @@ jobs: - id: stop-early run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi # The `check-for-functional-changes` job should always succeed regardless of the `has-functional-changes` script's exit code. Consequently, we do not use that exit code to trigger deploy, but rather a dedicated output variable `status`, to avoid a job failure if the exit code is different from 0. Conversely, if the job fails the entire workflow would be marked as `failed` which is disturbing for contributors. - deploy-doc: # TESTING - runs-on: ubuntu-latest - steps: - - name: Update doc - run: | - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ - -d '{"ref":"cd"}' - deploy: runs-on: ubuntu-22.04 needs: [ check-for-functional-changes ] From 005c5c6e2002bd4c76efe6538421ecf04f662b6a Mon Sep 17 00:00:00 2001 From: Matti Schneider Date: Tue, 21 May 2024 20:49:27 +0200 Subject: [PATCH 087/188] Bump version number --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8f7d51de..05e7e5c98c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.4.7 [#1212](https://github.com/openfisca/openfisca-core/pull/1212) + +#### Technical changes + +- Update documentation continuous deployment method to reflect OpenFisca-Doc [process updates](https://github.com/openfisca/openfisca-doc/pull/308) + ### 41.4.6 [#1210](https://github.com/openfisca/openfisca-core/pull/1210) #### Technical changes diff --git a/setup.py b/setup.py index cb41e7ec88..8135f9d881 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.4.6", + version="41.4.7", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 80c1deb4f7bedcebfeb45716aeb4feb2e7477622 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Wed, 17 Feb 2021 11:57:16 +0100 Subject: [PATCH 088/188] Introduce asof for by-date parameters --- openfisca_core/parameters/__init__.py | 2 + .../parameters/parameter_node_at_instant.py | 4 + ...ial_asof_date_parameter_node_at_instant.py | 70 ++++++++ .../core/parameters_date_indexing/__init__.py | 0 .../core/parameters_date_indexing/aad_rg.yaml | 121 +++++++++++++ .../test_date_indexing.py | 38 ++++ .../parameters_date_indexing/trimtp_rg.yml | 162 ++++++++++++++++++ 7 files changed, 397 insertions(+) create mode 100644 openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py create mode 100644 tests/core/parameters_date_indexing/__init__.py create mode 100644 tests/core/parameters_date_indexing/aad_rg.yaml create mode 100644 tests/core/parameters_date_indexing/test_date_indexing.py create mode 100644 tests/core/parameters_date_indexing/trimtp_rg.yml diff --git a/openfisca_core/parameters/__init__.py b/openfisca_core/parameters/__init__.py index a5ac18044f..33a1f06c65 100644 --- a/openfisca_core/parameters/__init__.py +++ b/openfisca_core/parameters/__init__.py @@ -41,6 +41,7 @@ from .parameter_scale_bracket import ParameterScaleBracket from .parameter_scale_bracket import ParameterScaleBracket as Bracket from .values_history import ValuesHistory +from .vectorial_asof_date_parameter_node_at_instant import VectorialAsofDateParameterNodeAtInstant from .vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant __all__ = [ @@ -63,5 +64,6 @@ "ParameterScaleBracket", "Bracket", "ValuesHistory", + "VectorialAsofDateParameterNodeAtInstant", "VectorialParameterNodeAtInstant", ] diff --git a/openfisca_core/parameters/parameter_node_at_instant.py b/openfisca_core/parameters/parameter_node_at_instant.py index 9dc0abee87..4eadfdd246 100644 --- a/openfisca_core/parameters/parameter_node_at_instant.py +++ b/openfisca_core/parameters/parameter_node_at_instant.py @@ -41,6 +41,10 @@ def __getattr__(self, key): def __getitem__(self, key): # If fancy indexing is used, cast to a vectorial node if isinstance(key, numpy.ndarray): + # If fancy indexing is used wit a datetime64, cast to a vectorial node supporting datetime64 + if numpy.issubdtype(key.dtype, numpy.datetime64): + return parameters.VectorialAsofDateParameterNodeAtInstant.build_from_node(self)[key] + return parameters.VectorialParameterNodeAtInstant.build_from_node(self)[key] return self._children[key] diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py new file mode 100644 index 0000000000..9b20bc86d7 --- /dev/null +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -0,0 +1,70 @@ +import numpy + +from openfisca_core import parameters +from openfisca_core.errors import ParameterNotFoundError +from openfisca_core.indexed_enums import Enum, EnumArray +from openfisca_core.parameters import helpers + + + + +class VectorialAsofDateParameterNodeAtInstant(parameters.VectorialParameterNodeAtInstant): + """ + Parameter node of the legislation at a given instant which has been vectorized along osme date. + Vectorized parameters allow requests such as parameters.housing_benefit[date], where date is a np.datetime64 type vector + """ + + @staticmethod + def build_from_node(node): + parameters.VectorialParameterNodeAtInstant.check_node_vectorisable(node) + subnodes_name = node._children.keys() + # Recursively vectorize the children of the node + vectorial_subnodes = tuple([ + VectorialAsofDateParameterNodeAtInstant.build_from_node(node[subnode_name]).vector + if isinstance(node[subnode_name], parameters.ParameterNodeAtInstant) + else node[subnode_name] + for subnode_name in subnodes_name + ]) + # A vectorial node is a wrapper around a numpy recarray + # We first build the recarray + recarray = numpy.array( + [vectorial_subnodes], + dtype = [ + (subnode_name, subnode.dtype if isinstance(subnode, numpy.recarray) else 'float') + for (subnode_name, subnode) in zip(subnodes_name, vectorial_subnodes) + ] + ) + return VectorialAsofDateParameterNodeAtInstant(node._name, recarray.view(numpy.recarray), node._instant_str) + + def __getitem__(self, key): + # If the key is a string, just get the subnode + if isinstance(key, str): + key = numpy.array([key], dtype = 'datetime64[D]') + return self.__getattr__(key) + # If the key is a vector, e.g. ['1990-11-25', '1983-04-17', '1969-09-09'] + elif isinstance(key, numpy.ndarray): + assert numpy.issubdtype(key.dtype, numpy.datetime64) + names = list(self.dtype.names) # Get all the names of the subnodes, e.g. ['ne_avant_X', 'ne_apres_X', 'ne_apres_Y'] + values = numpy.asarray([value for value in self.vector[0]]) + names = [ + name + for name in names + if not name.startswith("ne_avant") + ] + names = [ + numpy.datetime64( + "-".join(name[9:].split("_")) + ) + for name in names + ] + conditions = sum([ + name <= key + for name in names + ]) + result = values[conditions] + + # If the result is not a leaf, wrap the result in a vectorial node. + if numpy.issubdtype(result.dtype, numpy.record): + return VectorialAsofDateParameterNodeAtInstant(self._name, result.view(numpy.recarray), self._instant_str) + + return result diff --git a/tests/core/parameters_date_indexing/__init__.py b/tests/core/parameters_date_indexing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/parameters_date_indexing/aad_rg.yaml b/tests/core/parameters_date_indexing/aad_rg.yaml new file mode 100644 index 0000000000..4c78a56b6b --- /dev/null +++ b/tests/core/parameters_date_indexing/aad_rg.yaml @@ -0,0 +1,121 @@ +description: Age d'annulation de la décote +age_annulation_decote_en_fonction_date_naissance: + description: Age d'annulation de la décote en fonction de la date de naissance + ne_avant_1951_07_01: + description: Né avant le 01/07/1951 + annee: + description: Année + values: + 1983-04-01: + value: 65.0 + mois: + description: Mois + values: + 1983-04-01: + value: 0.0 + ne_apres_1951_07_01: + description: Né après le 01/07/1951 + annee: + description: Année + values: + 2011-07-01: + value: 65.0 + 1983-04-01: + value: null + mois: + description: Mois + values: + 2011-07-01: + value: 4.0 + 1983-04-01: + value: null + ne_apres_1952_01_01: + description: Né après le 01/01/1952 + annee: + description: Année + values: + 2011-07-01: + value: 65.0 + 1983-04-01: + value: null + mois: + description: Mois + values: + 2012-01-01: + value: 9.0 + 2011-07-01: + value: 8.0 + 1983-04-01: + value: null + ne_apres_1953_01_01: + description: Né après le 01/01/1953 + annee: + description: Année + values: + 2011-07-01: + value: 66.0 + 1983-04-01: + value: null + mois: + description: Mois + values: + 2012-01-01: + value: 2.0 + 2011-07-01: + value: 0.0 + 1983-04-01: + value: null + ne_apres_1954_01_01: + description: Né après le 01/01/1954 + annee: + description: Année + values: + 2011-07-01: + value: 66.0 + 1983-04-01: + value: null + mois: + description: Mois + values: + 2012-01-01: + value: 7.0 + 2011-07-01: + value: 4.0 + 1983-04-01: + value: null + ne_apres_1955_01_01: + description: Né après le 01/01/1955 + annee: + description: Année + values: + 2012-01-01: + value: 67.0 + 2011-07-01: + value: 66.0 + 1983-04-01: + value: null + mois: + description: Mois + values: + 2012-01-01: + value: 0.0 + 2011-07-01: + value: 8.0 + 1983-04-01: + value: null + ne_apres_1956_01_01: + description: Né après le 01/01/1956 + annee: + description: Année + values: + 2011-07-01: + value: 67.0 + 1983-04-01: + value: null + mois: + description: Mois + values: + 2011-07-01: + value: 0.0 + 1983-04-01: + value: null diff --git a/tests/core/parameters_date_indexing/test_date_indexing.py b/tests/core/parameters_date_indexing/test_date_indexing.py new file mode 100644 index 0000000000..5ca156e6b0 --- /dev/null +++ b/tests/core/parameters_date_indexing/test_date_indexing.py @@ -0,0 +1,38 @@ +import numpy +import os + + +from openfisca_core.tools import assert_near +from openfisca_core.parameters import ParameterNode +from openfisca_core.model_api import * # noqa + +LOCAL_DIR = os.path.dirname(os.path.abspath(__file__)) + +parameters = ParameterNode(directory_path = LOCAL_DIR) + + +def get_message(error): + return error.args[0] + + +def test_on_leaf(): + parameter_at_instant = parameters.trimtp_rg('1995-01-01') + date_de_naissance = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype = 'datetime64[D]') + assert_near(parameter_at_instant.nombre_trimestres_cibles_par_generation[date_de_naissance], [150, 152, 157, 160]) + + +def test_on_node(): + date_de_naissance = numpy.array(['1950-01-01', '1953-01-01', '1956-01-01', '1959-01-01'], dtype = 'datetime64[D]') + parameter_at_instant = parameters.aad_rg('2012-03-01') + node = parameter_at_instant.age_annulation_decote_en_fonction_date_naissance[date_de_naissance] + assert_near(node.annee, [65, 66, 67, 67]) + assert_near(node.mois, [0, 2, 0, 0]) + + +# def test_inhomogenous(): +# date_de_naissance = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype = 'datetime64[D]') +# parameter_at_instant = parameters.aad_rg('2011-01-01') +# parameter_at_instant.age_annulation_decote_en_fonction_date_naissance[date_de_naissance] +# with pytest.raises(ValueError) as error: +# parameter_at_instant.age_annulation_decote_en_fonction_date_naissance[date_de_naissance] +# assert "Cannot use fancy indexing on parameter node 'aad_rg.age_annulation_decote_en_fonction_date_naissance'" in get_message(error.value) diff --git a/tests/core/parameters_date_indexing/trimtp_rg.yml b/tests/core/parameters_date_indexing/trimtp_rg.yml new file mode 100644 index 0000000000..3ab6ad6cca --- /dev/null +++ b/tests/core/parameters_date_indexing/trimtp_rg.yml @@ -0,0 +1,162 @@ +description: Durée d'assurance cible pour le taux plein +nombre_trimestres_cibles_par_generation: + description: Nombre de trimestres cibles par génération + ne_avant_1934_01_01: + description: Avant 1934 + values: + 1983-01-01: + value: 150.0 + ne_apres_1934_01_01: + description: '1934-01-01' + values: + 1994-01-01: + value: 151.0 + 1983-01-01: + value: null + ne_apres_1935_01_01: + description: '1935-01-01' + values: + 1994-01-01: + value: 152.0 + 1983-01-01: + value: null + ne_apres_1936_01_01: + description: '1936-01-01' + values: + 1994-01-01: + value: 153.0 + 1983-01-01: + value: null + ne_apres_1937_01_01: + description: '1937-01-01' + values: + 1994-01-01: + value: 154.0 + 1983-01-01: + value: null + ne_apres_1938_01_01: + description: '1938-01-01' + values: + 1994-01-01: + value: 155.0 + 1983-01-01: + value: null + ne_apres_1939_01_01: + description: '1939-01-01' + values: + 1994-01-01: + value: 156.0 + 1983-01-01: + value: null + ne_apres_1940_01_01: + description: '1940-01-01' + values: + 1994-01-01: + value: 157.0 + 1983-01-01: + value: null + ne_apres_1941_01_01: + description: '1941-01-01' + values: + 1994-01-01: + value: 158.0 + 1983-01-01: + value: null + ne_apres_1942_01_01: + description: '1942-01-01' + values: + 1994-01-01: + value: 159.0 + 1983-01-01: + value: null + ne_apres_1943_01_01: + description: '1943-01-01' + values: + 1994-01-01: + value: 160.0 + 1983-01-01: + value: null + ne_apres_1949_01_01: + description: '1949-01-01' + values: + 2009-01-01: + value: 161.0 + 1983-01-01: + value: null + ne_apres_1950_01_01: + description: '1950-01-01' + values: + 2009-01-01: + value: 162.0 + 1983-01-01: + value: null + ne_apres_1951_01_01: + description: '1951-01-01' + values: + 2009-01-01: + value: 163.0 + 1983-01-01: + value: null + ne_apres_1952_01_01: + description: '1952-01-01' + values: + 2009-01-01: + value: 164.0 + 1983-01-01: + value: null + ne_apres_1953_01_01: + description: '1953-01-01' + values: + 2012-01-01: + value: 165.0 + 1983-01-01: + value: null + ne_apres_1955_01_01: + description: '1955-01-01' + values: + 2013-01-01: + value: 166.0 + 1983-01-01: + value: null + ne_apres_1958_01_01: + description: '1958-01-01' + values: + 2015-01-01: + value: 167.0 + 1983-01-01: + value: null + ne_apres_1961_01_01: + description: '1961-01-01' + values: + 2015-01-01: + value: 168.0 + 1983-01-01: + value: null + ne_apres_1964_01_01: + description: '1964-01-01' + values: + 2015-01-01: + value: 169.0 + 1983-01-01: + value: null + ne_apres_1967_01_01: + description: '1967-01-01' + values: + 2015-01-01: + value: 170.0 + 1983-01-01: + value: null + ne_apres_1970_01_01: + description: '1970-01-01' + values: + 2015-01-01: + value: 171.0 + 1983-01-01: + value: null + ne_apres_1973_01_01: + description: '1973-01-01' + values: + 2015-01-01: + value: 172.0 + 1983-01-01: + value: null From adff069bae41c364fabf8413181d3ef3aa9179f2 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Mon, 12 Apr 2021 22:45:37 +0200 Subject: [PATCH 089/188] flake8 --- .../parameters/parameter_node_at_instant.py | 2 +- ...ial_asof_date_parameter_node_at_instant.py | 5 - .../coefficient_de_minoration.yaml | 135 ++++++++++++++++++ 3 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 tests/core/parameters_fancy_indexing/coefficient_de_minoration.yaml diff --git a/openfisca_core/parameters/parameter_node_at_instant.py b/openfisca_core/parameters/parameter_node_at_instant.py index 4eadfdd246..40317f5846 100644 --- a/openfisca_core/parameters/parameter_node_at_instant.py +++ b/openfisca_core/parameters/parameter_node_at_instant.py @@ -41,7 +41,7 @@ def __getattr__(self, key): def __getitem__(self, key): # If fancy indexing is used, cast to a vectorial node if isinstance(key, numpy.ndarray): - # If fancy indexing is used wit a datetime64, cast to a vectorial node supporting datetime64 + # If fancy indexing is used wit a datetime64, cast to a vectorial node supporting datetime64 if numpy.issubdtype(key.dtype, numpy.datetime64): return parameters.VectorialAsofDateParameterNodeAtInstant.build_from_node(self)[key] diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index 9b20bc86d7..132d51afa0 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -1,11 +1,6 @@ import numpy from openfisca_core import parameters -from openfisca_core.errors import ParameterNotFoundError -from openfisca_core.indexed_enums import Enum, EnumArray -from openfisca_core.parameters import helpers - - class VectorialAsofDateParameterNodeAtInstant(parameters.VectorialParameterNodeAtInstant): diff --git a/tests/core/parameters_fancy_indexing/coefficient_de_minoration.yaml b/tests/core/parameters_fancy_indexing/coefficient_de_minoration.yaml new file mode 100644 index 0000000000..9894ae64aa --- /dev/null +++ b/tests/core/parameters_fancy_indexing/coefficient_de_minoration.yaml @@ -0,0 +1,135 @@ +description: Coefficient de minoration ARRCO +coefficient_minoration_en_fonction_distance_age_annulation_decote_en_annee: + description: Coefficient de minoration à l'Arrco en fonction de la distance à l'âge d'annulation de la décote (en année) + '-10': + description: '-10' + values: + 1965-01-01: + value: 0.43 + 1957-05-15: + value: null + '-9': + description: '-9' + values: + 1965-01-01: + value: 0.5 + 1957-05-15: + value: null + '-8': + description: '-8' + values: + 1965-01-01: + value: 0.57 + 1957-05-15: + value: null + '-7': + description: '-7' + values: + 1965-01-01: + value: 0.64 + 1957-05-15: + value: null + '-6': + description: '-6' + values: + 1965-01-01: + value: 0.71 + 1957-05-15: + value: null + '-5': + description: '-5' + values: + 1965-01-01: + value: 0.78 + 1957-05-15: + value: 0.75 + '-4': + description: '-4' + values: + 1965-01-01: + value: 0.83 + 1957-05-15: + value: 0.8 + '-3': + description: '-3' + values: + 1965-01-01: + value: 0.88 + 1957-05-15: + value: 0.85 + '-2': + description: '-2' + values: + 1965-01-01: + value: 0.92 + 1957-05-15: + value: 0.9 + '-1': + description: '-1' + values: + 1965-01-01: + value: 0.96 + 1957-05-15: + value: 0.95 + '0': + description: '0' + values: + 1965-01-01: + value: 1.0 + 1957-05-15: + value: 1.05 + '1': + description: '1' + values: + 1965-01-01: + value: null + 1957-05-15: + value: 1.1 + '2': + description: '2' + values: + 1965-01-01: + value: null + 1957-05-15: + value: 1.15 + '3': + description: '3' + values: + 1965-01-01: + value: null + 1957-05-15: + value: 1.2 + '4': + description: '4' + values: + 1965-01-01: + value: null + 1957-05-15: + value: 1.25 + metadata: + order: + - '-10' + - '-9' + - '-8' + - '-7' + - '-6' + - '-5' + - '-4' + - '-3' + - '-2' + - '-1' + - '0' + - '1' + - '2' + - '3' + - '4' +metadata: + order: + - coefficient_minoration_en_fonction_distance_age_annulation_decote_en_annee + reference: + 1965-01-01: Article 18 de l'annexe A de l'Accord national interprofessionnel de retraite complémentaire du 8 décembre 1961 + 1957-05-15: Accord du 15/05/1957 pour la création de l'UNIRS + description_en: Penalty for early retirement ARRCO +documentation: | + Note: Le coefficient d'abattement (ou de majoration avant 1965) constitue une multiplication des droits de pension à l'arrco par le coefficient en question. Par exemple, un individu partant en retraite à 60 ans en 1960 touchait 75% de sa pension. A partir de 1983, une double condition d'âge et de durée d'assurance est instaurée: un individu ayant validé une durée égale à la durée d'assurance cible(voir onglet Trim_tx_plein_RG) partira sans abbattement, même s'il n'a pas atteint l'âge d'annulation de la décôte dans le régime général (voir onglet Age_ann_dec_RG). + Note : le coefficient de minoration est linéaire en nombre de trimestres, e.g. il est de 0,43 à AAD - 10 ans, de 0,4475 à AAD - 9 ans et 3 trimestres, de 0,465 à AAD - 9 ans et 2 trimestres, etc. From 67a954a439a5051ae84a6c1d26f638117ccb19fa Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Tue, 14 Dec 2021 22:38:02 +0100 Subject: [PATCH 090/188] Use english prefix --- ...ial_asof_date_parameter_node_at_instant.py | 6 +-- .../core/parameters_date_indexing/aad_rg.yaml | 14 +++--- .../parameters_date_indexing/trimtp_rg.yml | 46 +++++++++---------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index 132d51afa0..8ce28e94f2 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -39,16 +39,16 @@ def __getitem__(self, key): # If the key is a vector, e.g. ['1990-11-25', '1983-04-17', '1969-09-09'] elif isinstance(key, numpy.ndarray): assert numpy.issubdtype(key.dtype, numpy.datetime64) - names = list(self.dtype.names) # Get all the names of the subnodes, e.g. ['ne_avant_X', 'ne_apres_X', 'ne_apres_Y'] + names = list(self.dtype.names) # Get all the names of the subnodes, e.g. ['before_X', 'after_X', 'after_Y'] values = numpy.asarray([value for value in self.vector[0]]) names = [ name for name in names - if not name.startswith("ne_avant") + if not name.startswith("before") ] names = [ numpy.datetime64( - "-".join(name[9:].split("_")) + "-".join(name[len("after_"):].split("_")) ) for name in names ] diff --git a/tests/core/parameters_date_indexing/aad_rg.yaml b/tests/core/parameters_date_indexing/aad_rg.yaml index 4c78a56b6b..9041fd1c15 100644 --- a/tests/core/parameters_date_indexing/aad_rg.yaml +++ b/tests/core/parameters_date_indexing/aad_rg.yaml @@ -1,7 +1,7 @@ description: Age d'annulation de la décote age_annulation_decote_en_fonction_date_naissance: description: Age d'annulation de la décote en fonction de la date de naissance - ne_avant_1951_07_01: + before_1951_07_01: description: Né avant le 01/07/1951 annee: description: Année @@ -13,7 +13,7 @@ age_annulation_decote_en_fonction_date_naissance: values: 1983-04-01: value: 0.0 - ne_apres_1951_07_01: + after_1951_07_01: description: Né après le 01/07/1951 annee: description: Année @@ -29,7 +29,7 @@ age_annulation_decote_en_fonction_date_naissance: value: 4.0 1983-04-01: value: null - ne_apres_1952_01_01: + after_1952_01_01: description: Né après le 01/01/1952 annee: description: Année @@ -47,7 +47,7 @@ age_annulation_decote_en_fonction_date_naissance: value: 8.0 1983-04-01: value: null - ne_apres_1953_01_01: + after_1953_01_01: description: Né après le 01/01/1953 annee: description: Année @@ -65,7 +65,7 @@ age_annulation_decote_en_fonction_date_naissance: value: 0.0 1983-04-01: value: null - ne_apres_1954_01_01: + after_1954_01_01: description: Né après le 01/01/1954 annee: description: Année @@ -83,7 +83,7 @@ age_annulation_decote_en_fonction_date_naissance: value: 4.0 1983-04-01: value: null - ne_apres_1955_01_01: + after_1955_01_01: description: Né après le 01/01/1955 annee: description: Année @@ -103,7 +103,7 @@ age_annulation_decote_en_fonction_date_naissance: value: 8.0 1983-04-01: value: null - ne_apres_1956_01_01: + after_1956_01_01: description: Né après le 01/01/1956 annee: description: Année diff --git a/tests/core/parameters_date_indexing/trimtp_rg.yml b/tests/core/parameters_date_indexing/trimtp_rg.yml index 3ab6ad6cca..42de94e86d 100644 --- a/tests/core/parameters_date_indexing/trimtp_rg.yml +++ b/tests/core/parameters_date_indexing/trimtp_rg.yml @@ -1,159 +1,159 @@ description: Durée d'assurance cible pour le taux plein nombre_trimestres_cibles_par_generation: description: Nombre de trimestres cibles par génération - ne_avant_1934_01_01: + before_1934_01_01: description: Avant 1934 values: 1983-01-01: value: 150.0 - ne_apres_1934_01_01: + after_1934_01_01: description: '1934-01-01' values: 1994-01-01: value: 151.0 1983-01-01: value: null - ne_apres_1935_01_01: + after_1935_01_01: description: '1935-01-01' values: 1994-01-01: value: 152.0 1983-01-01: value: null - ne_apres_1936_01_01: + after_1936_01_01: description: '1936-01-01' values: 1994-01-01: value: 153.0 1983-01-01: value: null - ne_apres_1937_01_01: + after_1937_01_01: description: '1937-01-01' values: 1994-01-01: value: 154.0 1983-01-01: value: null - ne_apres_1938_01_01: + after_1938_01_01: description: '1938-01-01' values: 1994-01-01: value: 155.0 1983-01-01: value: null - ne_apres_1939_01_01: + after_1939_01_01: description: '1939-01-01' values: 1994-01-01: value: 156.0 1983-01-01: value: null - ne_apres_1940_01_01: + after_1940_01_01: description: '1940-01-01' values: 1994-01-01: value: 157.0 1983-01-01: value: null - ne_apres_1941_01_01: + after_1941_01_01: description: '1941-01-01' values: 1994-01-01: value: 158.0 1983-01-01: value: null - ne_apres_1942_01_01: + after_1942_01_01: description: '1942-01-01' values: 1994-01-01: value: 159.0 1983-01-01: value: null - ne_apres_1943_01_01: + after_1943_01_01: description: '1943-01-01' values: 1994-01-01: value: 160.0 1983-01-01: value: null - ne_apres_1949_01_01: + after_1949_01_01: description: '1949-01-01' values: 2009-01-01: value: 161.0 1983-01-01: value: null - ne_apres_1950_01_01: + after_1950_01_01: description: '1950-01-01' values: 2009-01-01: value: 162.0 1983-01-01: value: null - ne_apres_1951_01_01: + after_1951_01_01: description: '1951-01-01' values: 2009-01-01: value: 163.0 1983-01-01: value: null - ne_apres_1952_01_01: + after_1952_01_01: description: '1952-01-01' values: 2009-01-01: value: 164.0 1983-01-01: value: null - ne_apres_1953_01_01: + after_1953_01_01: description: '1953-01-01' values: 2012-01-01: value: 165.0 1983-01-01: value: null - ne_apres_1955_01_01: + after_1955_01_01: description: '1955-01-01' values: 2013-01-01: value: 166.0 1983-01-01: value: null - ne_apres_1958_01_01: + after_1958_01_01: description: '1958-01-01' values: 2015-01-01: value: 167.0 1983-01-01: value: null - ne_apres_1961_01_01: + after_1961_01_01: description: '1961-01-01' values: 2015-01-01: value: 168.0 1983-01-01: value: null - ne_apres_1964_01_01: + after_1964_01_01: description: '1964-01-01' values: 2015-01-01: value: 169.0 1983-01-01: value: null - ne_apres_1967_01_01: + after_1967_01_01: description: '1967-01-01' values: 2015-01-01: value: 170.0 1983-01-01: value: null - ne_apres_1970_01_01: + after_1970_01_01: description: '1970-01-01' values: 2015-01-01: value: 171.0 1983-01-01: value: null - ne_apres_1973_01_01: + after_1973_01_01: description: '1973-01-01' values: 2015-01-01: From 9649253221307bfc2838ef1260ef4b0f22d890c9 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 18 Mar 2022 22:26:49 +0100 Subject: [PATCH 091/188] Translate parameters into english --- .../{aad_rg.yaml => full_rate_age.yaml} | 76 +++++++++---------- ...rg.yml => full_rate_required_duration.yml} | 8 +- .../test_date_indexing.py | 24 +++--- 3 files changed, 54 insertions(+), 54 deletions(-) rename tests/core/parameters_date_indexing/{aad_rg.yaml => full_rate_age.yaml} (62%) rename tests/core/parameters_date_indexing/{trimtp_rg.yml => full_rate_required_duration.yml} (93%) diff --git a/tests/core/parameters_date_indexing/aad_rg.yaml b/tests/core/parameters_date_indexing/full_rate_age.yaml similarity index 62% rename from tests/core/parameters_date_indexing/aad_rg.yaml rename to tests/core/parameters_date_indexing/full_rate_age.yaml index 9041fd1c15..4e7a91c866 100644 --- a/tests/core/parameters_date_indexing/aad_rg.yaml +++ b/tests/core/parameters_date_indexing/full_rate_age.yaml @@ -1,45 +1,45 @@ -description: Age d'annulation de la décote -age_annulation_decote_en_fonction_date_naissance: - description: Age d'annulation de la décote en fonction de la date de naissance +description: Full rate age +full_rate_age_by_birthdateage_annulation_decote_en_fonction_date_naissance: + description: Full rate age by birthdate before_1951_07_01: - description: Né avant le 01/07/1951 - annee: - description: Année + description: Born before 01/07/1951 + year: + description: Year values: 1983-04-01: value: 65.0 - mois: - description: Mois + month: + description: Month values: 1983-04-01: value: 0.0 after_1951_07_01: - description: Né après le 01/07/1951 - annee: - description: Année + description: Born after 01/07/1951 + year: + description: Year values: 2011-07-01: value: 65.0 1983-04-01: value: null - mois: - description: Mois + month: + description: Month values: 2011-07-01: value: 4.0 1983-04-01: value: null after_1952_01_01: - description: Né après le 01/01/1952 - annee: - description: Année + description: Born after 01/01/1952 + year: + description: Year values: 2011-07-01: value: 65.0 1983-04-01: value: null - mois: - description: Mois + month: + description: Month values: 2012-01-01: value: 9.0 @@ -48,16 +48,16 @@ age_annulation_decote_en_fonction_date_naissance: 1983-04-01: value: null after_1953_01_01: - description: Né après le 01/01/1953 - annee: - description: Année + description: Born after 01/01/1953 + year: + description: Year values: 2011-07-01: value: 66.0 1983-04-01: value: null - mois: - description: Mois + month: + description: Month values: 2012-01-01: value: 2.0 @@ -66,16 +66,16 @@ age_annulation_decote_en_fonction_date_naissance: 1983-04-01: value: null after_1954_01_01: - description: Né après le 01/01/1954 - annee: - description: Année + description: Born after 01/01/1954 + year: + description: Year values: 2011-07-01: value: 66.0 1983-04-01: value: null - mois: - description: Mois + month: + description: Month values: 2012-01-01: value: 7.0 @@ -84,9 +84,9 @@ age_annulation_decote_en_fonction_date_naissance: 1983-04-01: value: null after_1955_01_01: - description: Né après le 01/01/1955 - annee: - description: Année + description: Born after 01/01/1955 + year: + description: Year values: 2012-01-01: value: 67.0 @@ -94,8 +94,8 @@ age_annulation_decote_en_fonction_date_naissance: value: 66.0 1983-04-01: value: null - mois: - description: Mois + month: + description: Month values: 2012-01-01: value: 0.0 @@ -104,16 +104,16 @@ age_annulation_decote_en_fonction_date_naissance: 1983-04-01: value: null after_1956_01_01: - description: Né après le 01/01/1956 - annee: - description: Année + description: Born after 01/01/1956 + year: + description: Year values: 2011-07-01: value: 67.0 1983-04-01: value: null - mois: - description: Mois + month: + description: Month values: 2011-07-01: value: 0.0 diff --git a/tests/core/parameters_date_indexing/trimtp_rg.yml b/tests/core/parameters_date_indexing/full_rate_required_duration.yml similarity index 93% rename from tests/core/parameters_date_indexing/trimtp_rg.yml rename to tests/core/parameters_date_indexing/full_rate_required_duration.yml index 42de94e86d..59a73cd90c 100644 --- a/tests/core/parameters_date_indexing/trimtp_rg.yml +++ b/tests/core/parameters_date_indexing/full_rate_required_duration.yml @@ -1,8 +1,8 @@ -description: Durée d'assurance cible pour le taux plein -nombre_trimestres_cibles_par_generation: - description: Nombre de trimestres cibles par génération +description: Required contribution duration for full rate +contribution_quarters_required_by_birthdate:nombre_trimestres_cibles_par_generation: + description: Contribution quarters required by birthdate before_1934_01_01: - description: Avant 1934 + description: before 1934 values: 1983-01-01: value: 150.0 diff --git a/tests/core/parameters_date_indexing/test_date_indexing.py b/tests/core/parameters_date_indexing/test_date_indexing.py index 5ca156e6b0..41621de8ca 100644 --- a/tests/core/parameters_date_indexing/test_date_indexing.py +++ b/tests/core/parameters_date_indexing/test_date_indexing.py @@ -17,22 +17,22 @@ def get_message(error): def test_on_leaf(): parameter_at_instant = parameters.trimtp_rg('1995-01-01') - date_de_naissance = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype = 'datetime64[D]') - assert_near(parameter_at_instant.nombre_trimestres_cibles_par_generation[date_de_naissance], [150, 152, 157, 160]) + birthdate = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype = 'datetime64[D]') + assert_near(parameter_at_instant.contribution_quarters_required_by_birthdate[birthdate], [150, 152, 157, 160]) def test_on_node(): - date_de_naissance = numpy.array(['1950-01-01', '1953-01-01', '1956-01-01', '1959-01-01'], dtype = 'datetime64[D]') - parameter_at_instant = parameters.aad_rg('2012-03-01') - node = parameter_at_instant.age_annulation_decote_en_fonction_date_naissance[date_de_naissance] - assert_near(node.annee, [65, 66, 67, 67]) - assert_near(node.mois, [0, 2, 0, 0]) + birthdate = numpy.array(['1950-01-01', '1953-01-01', '1956-01-01', '1959-01-01'], dtype = 'datetime64[D]') + parameter_at_instant = parameters.full_rate_age('2012-03-01') + node = parameter_at_instant.full_rate_age_by_birthdate[birthdate] + assert_near(node.year, [65, 66, 67, 67]) + assert_near(node.month, [0, 2, 0, 0]) # def test_inhomogenous(): -# date_de_naissance = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype = 'datetime64[D]') -# parameter_at_instant = parameters.aad_rg('2011-01-01') -# parameter_at_instant.age_annulation_decote_en_fonction_date_naissance[date_de_naissance] +# birthdate = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype = 'datetime64[D]') +# parameter_at_instant = parameters..full_rate_age('2011-01-01') +# parameter_at_instant.full_rate_age_by_birthdate[birthdate] # with pytest.raises(ValueError) as error: -# parameter_at_instant.age_annulation_decote_en_fonction_date_naissance[date_de_naissance] -# assert "Cannot use fancy indexing on parameter node 'aad_rg.age_annulation_decote_en_fonction_date_naissance'" in get_message(error.value) +# parameter_at_instant.full_rate_age_by_birthdate[birthdate] +# assert "Cannot use fancy indexing on parameter node '.full_rate_age.full_rate_age_by_birthdate'" in get_message(error.value) From 5a271eac5a11bdc76221b7a992a16a8b5ec49780 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 18 Mar 2022 22:34:09 +0100 Subject: [PATCH 092/188] Typo --- tests/core/parameters_date_indexing/full_rate_age.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/parameters_date_indexing/full_rate_age.yaml b/tests/core/parameters_date_indexing/full_rate_age.yaml index 4e7a91c866..fa9377fec5 100644 --- a/tests/core/parameters_date_indexing/full_rate_age.yaml +++ b/tests/core/parameters_date_indexing/full_rate_age.yaml @@ -1,5 +1,5 @@ description: Full rate age -full_rate_age_by_birthdateage_annulation_decote_en_fonction_date_naissance: +full_rate_age_by_birthdate: description: Full rate age by birthdate before_1951_07_01: description: Born before 01/07/1951 From c75120d20b4c5182bf59f868021f397de731541e Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 18 Mar 2022 22:35:25 +0100 Subject: [PATCH 093/188] Missing translation --- tests/core/parameters_date_indexing/test_date_indexing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/parameters_date_indexing/test_date_indexing.py b/tests/core/parameters_date_indexing/test_date_indexing.py index 41621de8ca..01e9c956cb 100644 --- a/tests/core/parameters_date_indexing/test_date_indexing.py +++ b/tests/core/parameters_date_indexing/test_date_indexing.py @@ -16,7 +16,7 @@ def get_message(error): def test_on_leaf(): - parameter_at_instant = parameters.trimtp_rg('1995-01-01') + parameter_at_instant = parameters.full_rate_required_duration('1995-01-01') birthdate = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype = 'datetime64[D]') assert_near(parameter_at_instant.contribution_quarters_required_by_birthdate[birthdate], [150, 152, 157, 160]) From 4e399273bd5e435f07d6fe16e0230e139defc43b Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 18 Mar 2022 23:35:58 +0100 Subject: [PATCH 094/188] Fix typo again --- .../parameters_date_indexing/full_rate_required_duration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/parameters_date_indexing/full_rate_required_duration.yml b/tests/core/parameters_date_indexing/full_rate_required_duration.yml index 59a73cd90c..af394ec568 100644 --- a/tests/core/parameters_date_indexing/full_rate_required_duration.yml +++ b/tests/core/parameters_date_indexing/full_rate_required_duration.yml @@ -1,5 +1,5 @@ description: Required contribution duration for full rate -contribution_quarters_required_by_birthdate:nombre_trimestres_cibles_par_generation: +contribution_quarters_required_by_birthdate: description: Contribution quarters required by birthdate before_1934_01_01: description: before 1934 From 724264075c6c78e4139775c7f379e0a2e634093b Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Mon, 8 Aug 2022 11:11:53 +0200 Subject: [PATCH 095/188] Introduce error margin by variable in tests --- openfisca_core/tools/test_runner.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/openfisca_core/tools/test_runner.py b/openfisca_core/tools/test_runner.py index 21987068e1..fe1baf8b3a 100644 --- a/openfisca_core/tools/test_runner.py +++ b/openfisca_core/tools/test_runner.py @@ -4,6 +4,7 @@ from typing import Any, Dict, Optional, Sequence, Union from typing_extensions import Literal, TypedDict +import collections import dataclasses import os import pathlib @@ -319,16 +320,30 @@ def check_variable( actual_value = self.simulation.calculate(variable_name, period) + absolute_error_margin = self.test.get('absolute_error_margin') + variable_absolute_error_margin = ( + absolute_error_margin.get(variable_name, absolute_error_margin.get("default")) + if isinstance(absolute_error_margin, collections.Mapping) + else absolute_error_margin + ) + + relative_error_margin = self.test.get('relative_error_margin') + variable_relative_error_margin = ( + relative_error_margin.get(variable_name, relative_error_margin.get("default")) + if isinstance(relative_error_margin, collections.Mapping) + else relative_error_margin + ) + if entity_index is not None: actual_value = actual_value[entity_index] return assert_near( actual_value, expected_value, - self.test.absolute_error_margin[variable_name], - f"{variable_name}@{period}: ", - self.test.relative_error_margin[variable_name], - ) + absolute_error_margin = variable_absolute_error_margin, + message = f"{variable_name}@{period}: ", + relative_error_margin = variable_relative_error_margin, + ) def should_ignore_variable(self, variable_name: str): only_variables = self.options.get("only_variables") From 752ea570fe8558d2bbeb91ca1e23f8db92f92d88 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Thu, 17 Aug 2023 09:10:15 +0200 Subject: [PATCH 096/188] Fix test and vectorisal asof parameters --- ...ial_asof_date_parameter_node_at_instant.py | 2 +- openfisca_core/tools/test_runner.py | 20 +++---------------- 2 files changed, 4 insertions(+), 18 deletions(-) diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index 8ce28e94f2..c4cd50b2ac 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -59,7 +59,7 @@ def __getitem__(self, key): result = values[conditions] # If the result is not a leaf, wrap the result in a vectorial node. - if numpy.issubdtype(result.dtype, numpy.record): + if numpy.issubdtype(result.dtype, numpy.record) or numpy.issubdtype(result.dtype, numpy.void): return VectorialAsofDateParameterNodeAtInstant(self._name, result.view(numpy.recarray), self._instant_str) return result diff --git a/openfisca_core/tools/test_runner.py b/openfisca_core/tools/test_runner.py index fe1baf8b3a..5bd6c009c1 100644 --- a/openfisca_core/tools/test_runner.py +++ b/openfisca_core/tools/test_runner.py @@ -320,29 +320,15 @@ def check_variable( actual_value = self.simulation.calculate(variable_name, period) - absolute_error_margin = self.test.get('absolute_error_margin') - variable_absolute_error_margin = ( - absolute_error_margin.get(variable_name, absolute_error_margin.get("default")) - if isinstance(absolute_error_margin, collections.Mapping) - else absolute_error_margin - ) - - relative_error_margin = self.test.get('relative_error_margin') - variable_relative_error_margin = ( - relative_error_margin.get(variable_name, relative_error_margin.get("default")) - if isinstance(relative_error_margin, collections.Mapping) - else relative_error_margin - ) - if entity_index is not None: actual_value = actual_value[entity_index] return assert_near( actual_value, expected_value, - absolute_error_margin = variable_absolute_error_margin, - message = f"{variable_name}@{period}: ", - relative_error_margin = variable_relative_error_margin, + self.test.absolute_error_margin[variable_name], + f"{variable_name}@{period}: ", + self.test.relative_error_margin[variable_name], ) def should_ignore_variable(self, variable_name: str): From 67f13ff87bd7826dda876338fbcd5e58ef6a4fdb Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 28 Jun 2024 09:45:16 +0100 Subject: [PATCH 097/188] Debug --- .../vectorial_asof_date_parameter_node_at_instant.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index c4cd50b2ac..c717790a0d 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -1,9 +1,9 @@ import numpy -from openfisca_core import parameters +from openfisca_core.parameters.vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant -class VectorialAsofDateParameterNodeAtInstant(parameters.VectorialParameterNodeAtInstant): +class VectorialAsofDateParameterNodeAtInstant(VectorialParameterNodeAtInstant): """ Parameter node of the legislation at a given instant which has been vectorized along osme date. Vectorized parameters allow requests such as parameters.housing_benefit[date], where date is a np.datetime64 type vector From 3dcb7738734ae39138b5ced45e6f041c9b016fc1 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 28 Jun 2024 09:57:31 +0100 Subject: [PATCH 098/188] lint --- tests/core/parameters_date_indexing/test_date_indexing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/core/parameters_date_indexing/test_date_indexing.py b/tests/core/parameters_date_indexing/test_date_indexing.py index 01e9c956cb..1b19c72bae 100644 --- a/tests/core/parameters_date_indexing/test_date_indexing.py +++ b/tests/core/parameters_date_indexing/test_date_indexing.py @@ -8,7 +8,7 @@ LOCAL_DIR = os.path.dirname(os.path.abspath(__file__)) -parameters = ParameterNode(directory_path = LOCAL_DIR) +parameters = ParameterNode(directory_path=LOCAL_DIR) def get_message(error): @@ -17,12 +17,12 @@ def get_message(error): def test_on_leaf(): parameter_at_instant = parameters.full_rate_required_duration('1995-01-01') - birthdate = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype = 'datetime64[D]') + birthdate = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype='datetime64[D]') assert_near(parameter_at_instant.contribution_quarters_required_by_birthdate[birthdate], [150, 152, 157, 160]) def test_on_node(): - birthdate = numpy.array(['1950-01-01', '1953-01-01', '1956-01-01', '1959-01-01'], dtype = 'datetime64[D]') + birthdate = numpy.array(['1950-01-01', '1953-01-01', '1956-01-01', '1959-01-01'], dtype='datetime64[D]') parameter_at_instant = parameters.full_rate_age('2012-03-01') node = parameter_at_instant.full_rate_age_by_birthdate[birthdate] assert_near(node.year, [65, 66, 67, 67]) From 050e488780cb3d54593add086bf601cf9ae5ce8a Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 28 Jun 2024 10:03:37 +0100 Subject: [PATCH 099/188] lint again --- .../vectorial_asof_date_parameter_node_at_instant.py | 4 ++-- openfisca_core/tools/test_runner.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index c717790a0d..9cc543eefb 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -24,7 +24,7 @@ def build_from_node(node): # We first build the recarray recarray = numpy.array( [vectorial_subnodes], - dtype = [ + dtype=[ (subnode_name, subnode.dtype if isinstance(subnode, numpy.recarray) else 'float') for (subnode_name, subnode) in zip(subnodes_name, vectorial_subnodes) ] @@ -34,7 +34,7 @@ def build_from_node(node): def __getitem__(self, key): # If the key is a string, just get the subnode if isinstance(key, str): - key = numpy.array([key], dtype = 'datetime64[D]') + key = numpy.array([key], dtype='datetime64[D]') return self.__getattr__(key) # If the key is a vector, e.g. ['1990-11-25', '1983-04-17', '1969-09-09'] elif isinstance(key, numpy.ndarray): diff --git a/openfisca_core/tools/test_runner.py b/openfisca_core/tools/test_runner.py index 5bd6c009c1..b2a564e1b0 100644 --- a/openfisca_core/tools/test_runner.py +++ b/openfisca_core/tools/test_runner.py @@ -4,7 +4,6 @@ from typing import Any, Dict, Optional, Sequence, Union from typing_extensions import Literal, TypedDict -import collections import dataclasses import os import pathlib From be86433cdeada6017e9cce3728c648a226089254 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 28 Jun 2024 10:16:36 +0100 Subject: [PATCH 100/188] lint again --- ...torial_asof_date_parameter_node_at_instant.py | 16 ++++++++-------- openfisca_core/tools/test_runner.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index 9cc543eefb..07bee71dfe 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -11,7 +11,7 @@ class VectorialAsofDateParameterNodeAtInstant(VectorialParameterNodeAtInstant): @staticmethod def build_from_node(node): - parameters.VectorialParameterNodeAtInstant.check_node_vectorisable(node) + VectorialParameterNodeAtInstant.check_node_vectorisable(node) subnodes_name = node._children.keys() # Recursively vectorize the children of the node vectorial_subnodes = tuple([ @@ -19,7 +19,7 @@ def build_from_node(node): if isinstance(node[subnode_name], parameters.ParameterNodeAtInstant) else node[subnode_name] for subnode_name in subnodes_name - ]) + ]) # A vectorial node is a wrapper around a numpy recarray # We first build the recarray recarray = numpy.array( @@ -27,8 +27,8 @@ def build_from_node(node): dtype=[ (subnode_name, subnode.dtype if isinstance(subnode, numpy.recarray) else 'float') for (subnode_name, subnode) in zip(subnodes_name, vectorial_subnodes) - ] - ) + ] + ) return VectorialAsofDateParameterNodeAtInstant(node._name, recarray.view(numpy.recarray), node._instant_str) def __getitem__(self, key): @@ -45,17 +45,17 @@ def __getitem__(self, key): name for name in names if not name.startswith("before") - ] + ] names = [ numpy.datetime64( "-".join(name[len("after_"):].split("_")) - ) + ) for name in names - ] + ] conditions = sum([ name <= key for name in names - ]) + ]) result = values[conditions] # If the result is not a leaf, wrap the result in a vectorial node. diff --git a/openfisca_core/tools/test_runner.py b/openfisca_core/tools/test_runner.py index b2a564e1b0..21987068e1 100644 --- a/openfisca_core/tools/test_runner.py +++ b/openfisca_core/tools/test_runner.py @@ -328,7 +328,7 @@ def check_variable( self.test.absolute_error_margin[variable_name], f"{variable_name}@{period}: ", self.test.relative_error_margin[variable_name], - ) + ) def should_ignore_variable(self, variable_name: str): only_variables = self.options.get("only_variables") From c4c075a004dd3e18aa7620848a198e9dee67b0bf Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 28 Jun 2024 10:28:26 +0100 Subject: [PATCH 101/188] lint again --- .../vectorial_asof_date_parameter_node_at_instant.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index 07bee71dfe..28a5dfe06d 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -1,5 +1,6 @@ import numpy +from openfisca_core.parameters.parameter_node_at_instant import ParameterNodeAtInstant from openfisca_core.parameters.vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant @@ -16,7 +17,7 @@ def build_from_node(node): # Recursively vectorize the children of the node vectorial_subnodes = tuple([ VectorialAsofDateParameterNodeAtInstant.build_from_node(node[subnode_name]).vector - if isinstance(node[subnode_name], parameters.ParameterNodeAtInstant) + if isinstance(node[subnode_name], ParameterNodeAtInstant) else node[subnode_name] for subnode_name in subnodes_name ]) From 2074e6c444f3bbbda9287e9ab3ab163ecfaafc2b Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 28 Jun 2024 11:06:36 +0100 Subject: [PATCH 102/188] Fix typo --- .../parameters/vectorial_asof_date_parameter_node_at_instant.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index 28a5dfe06d..8b7dd0fdde 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -6,7 +6,7 @@ class VectorialAsofDateParameterNodeAtInstant(VectorialParameterNodeAtInstant): """ - Parameter node of the legislation at a given instant which has been vectorized along osme date. + Parameter node of the legislation at a given instant which has been vectorized along some date. Vectorized parameters allow requests such as parameters.housing_benefit[date], where date is a np.datetime64 type vector """ From bc4ac9efebf1d4638b551a73a6cced930caae2b0 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Fri, 28 Jun 2024 11:10:42 +0100 Subject: [PATCH 103/188] Bump --- CHANGELOG.md | 10 +++++++++- setup.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05e7e5c98c..99a085b4cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog -### 41.4.7 [#1212](https://github.com/openfisca/openfisca-core/pull/1212) +## 41.5.0 [#1212](https://github.com/openfisca/openfisca-core/pull/1212) + +#### New features + +- Introduce `VectorialAsofDateParameterNodeAtInstant` + - It is a parameter node of the legislation at a given instant which has been vectorized along some date. + - Vectorized parameters allow requests such as parameters.housing_benefit[date], where date is a `numpy.datetime64` vector + +### 41.4.7 [#1211](https://github.com/openfisca/openfisca-core/pull/1211) #### Technical changes diff --git a/setup.py b/setup.py index 8135f9d881..ceb862e0d7 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.4.7", + version="41.5.0", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 80f0bea6e72e7d34dadd407f9ae0d5e0e0a601f8 Mon Sep 17 00:00:00 2001 From: Mahdi Ben Jelloul Date: Mon, 5 Aug 2024 15:05:04 +0200 Subject: [PATCH 104/188] Bump twine version to allow deployment --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ceb862e0d7..e66da731b1 100644 --- a/setup.py +++ b/setup.py @@ -104,7 +104,7 @@ "ci": [ "build >=0.10.0, < 0.11.0", "coveralls >=3.3.1, < 4.0", - "twine >=4.0.2, < 5.0", + "twine >=5.1.1, < 6.0", "wheel >=0.40.0, < 0.41.0", ], "tracker": ["OpenFisca-Tracker >=0.4.0, < 0.5.0"], From b7d13347715c5fe274798729be0e54df277b2f75 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 18:44:39 +0200 Subject: [PATCH 105/188] build(black): add dry-run check --- openfisca_tasks/lint.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index c937dd4d0b..a46b4d7c43 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -11,6 +11,7 @@ check-syntax-errors: . ## Run linters to check for syntax and style errors. check-style: $(shell git ls-files "*.py") @$(call print_help,$@:) + @black --check $? @flake8 $? @$(call print_pass,$@:) From 4dbf2467caf6250aeb88f82bf3741fc525ec6039 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 18:45:34 +0200 Subject: [PATCH 106/188] fix(black): linting errors --- openfisca_core/parameters/__init__.py | 4 +- .../parameters/parameter_node_at_instant.py | 6 +- ...ial_asof_date_parameter_node_at_instant.py | 60 +++++++++++-------- .../test_date_indexing.py | 23 ++++--- 4 files changed, 57 insertions(+), 36 deletions(-) diff --git a/openfisca_core/parameters/__init__.py b/openfisca_core/parameters/__init__.py index 33a1f06c65..f64f577fd4 100644 --- a/openfisca_core/parameters/__init__.py +++ b/openfisca_core/parameters/__init__.py @@ -41,7 +41,9 @@ from .parameter_scale_bracket import ParameterScaleBracket from .parameter_scale_bracket import ParameterScaleBracket as Bracket from .values_history import ValuesHistory -from .vectorial_asof_date_parameter_node_at_instant import VectorialAsofDateParameterNodeAtInstant +from .vectorial_asof_date_parameter_node_at_instant import ( + VectorialAsofDateParameterNodeAtInstant, +) from .vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant __all__ = [ diff --git a/openfisca_core/parameters/parameter_node_at_instant.py b/openfisca_core/parameters/parameter_node_at_instant.py index 40317f5846..d98d88a698 100644 --- a/openfisca_core/parameters/parameter_node_at_instant.py +++ b/openfisca_core/parameters/parameter_node_at_instant.py @@ -43,7 +43,11 @@ def __getitem__(self, key): if isinstance(key, numpy.ndarray): # If fancy indexing is used wit a datetime64, cast to a vectorial node supporting datetime64 if numpy.issubdtype(key.dtype, numpy.datetime64): - return parameters.VectorialAsofDateParameterNodeAtInstant.build_from_node(self)[key] + return ( + parameters.VectorialAsofDateParameterNodeAtInstant.build_from_node( + self + )[key] + ) return parameters.VectorialParameterNodeAtInstant.build_from_node(self)[key] return self._children[key] diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index 8b7dd0fdde..e00ce11733 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -1,7 +1,9 @@ import numpy from openfisca_core.parameters.parameter_node_at_instant import ParameterNodeAtInstant -from openfisca_core.parameters.vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant +from openfisca_core.parameters.vectorial_parameter_node_at_instant import ( + VectorialParameterNodeAtInstant, +) class VectorialAsofDateParameterNodeAtInstant(VectorialParameterNodeAtInstant): @@ -15,52 +17,58 @@ def build_from_node(node): VectorialParameterNodeAtInstant.check_node_vectorisable(node) subnodes_name = node._children.keys() # Recursively vectorize the children of the node - vectorial_subnodes = tuple([ - VectorialAsofDateParameterNodeAtInstant.build_from_node(node[subnode_name]).vector - if isinstance(node[subnode_name], ParameterNodeAtInstant) - else node[subnode_name] - for subnode_name in subnodes_name - ]) + vectorial_subnodes = tuple( + [ + VectorialAsofDateParameterNodeAtInstant.build_from_node( + node[subnode_name] + ).vector + if isinstance(node[subnode_name], ParameterNodeAtInstant) + else node[subnode_name] + for subnode_name in subnodes_name + ] + ) # A vectorial node is a wrapper around a numpy recarray # We first build the recarray recarray = numpy.array( [vectorial_subnodes], dtype=[ - (subnode_name, subnode.dtype if isinstance(subnode, numpy.recarray) else 'float') + ( + subnode_name, + subnode.dtype if isinstance(subnode, numpy.recarray) else "float", + ) for (subnode_name, subnode) in zip(subnodes_name, vectorial_subnodes) - ] + ], + ) + return VectorialAsofDateParameterNodeAtInstant( + node._name, recarray.view(numpy.recarray), node._instant_str ) - return VectorialAsofDateParameterNodeAtInstant(node._name, recarray.view(numpy.recarray), node._instant_str) def __getitem__(self, key): # If the key is a string, just get the subnode if isinstance(key, str): - key = numpy.array([key], dtype='datetime64[D]') + key = numpy.array([key], dtype="datetime64[D]") return self.__getattr__(key) # If the key is a vector, e.g. ['1990-11-25', '1983-04-17', '1969-09-09'] elif isinstance(key, numpy.ndarray): assert numpy.issubdtype(key.dtype, numpy.datetime64) - names = list(self.dtype.names) # Get all the names of the subnodes, e.g. ['before_X', 'after_X', 'after_Y'] + names = list( + self.dtype.names + ) # Get all the names of the subnodes, e.g. ['before_X', 'after_X', 'after_Y'] values = numpy.asarray([value for value in self.vector[0]]) + names = [name for name in names if not name.startswith("before")] names = [ - name + numpy.datetime64("-".join(name[len("after_") :].split("_"))) for name in names - if not name.startswith("before") ] - names = [ - numpy.datetime64( - "-".join(name[len("after_"):].split("_")) - ) - for name in names - ] - conditions = sum([ - name <= key - for name in names - ]) + conditions = sum([name <= key for name in names]) result = values[conditions] # If the result is not a leaf, wrap the result in a vectorial node. - if numpy.issubdtype(result.dtype, numpy.record) or numpy.issubdtype(result.dtype, numpy.void): - return VectorialAsofDateParameterNodeAtInstant(self._name, result.view(numpy.recarray), self._instant_str) + if numpy.issubdtype(result.dtype, numpy.record) or numpy.issubdtype( + result.dtype, numpy.void + ): + return VectorialAsofDateParameterNodeAtInstant( + self._name, result.view(numpy.recarray), self._instant_str + ) return result diff --git a/tests/core/parameters_date_indexing/test_date_indexing.py b/tests/core/parameters_date_indexing/test_date_indexing.py index 1b19c72bae..35f5dfd477 100644 --- a/tests/core/parameters_date_indexing/test_date_indexing.py +++ b/tests/core/parameters_date_indexing/test_date_indexing.py @@ -1,10 +1,10 @@ -import numpy import os +import numpy -from openfisca_core.tools import assert_near -from openfisca_core.parameters import ParameterNode from openfisca_core.model_api import * # noqa +from openfisca_core.parameters import ParameterNode +from openfisca_core.tools import assert_near LOCAL_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -16,14 +16,21 @@ def get_message(error): def test_on_leaf(): - parameter_at_instant = parameters.full_rate_required_duration('1995-01-01') - birthdate = numpy.array(['1930-01-01', '1935-01-01', '1940-01-01', '1945-01-01'], dtype='datetime64[D]') - assert_near(parameter_at_instant.contribution_quarters_required_by_birthdate[birthdate], [150, 152, 157, 160]) + parameter_at_instant = parameters.full_rate_required_duration("1995-01-01") + birthdate = numpy.array( + ["1930-01-01", "1935-01-01", "1940-01-01", "1945-01-01"], dtype="datetime64[D]" + ) + assert_near( + parameter_at_instant.contribution_quarters_required_by_birthdate[birthdate], + [150, 152, 157, 160], + ) def test_on_node(): - birthdate = numpy.array(['1950-01-01', '1953-01-01', '1956-01-01', '1959-01-01'], dtype='datetime64[D]') - parameter_at_instant = parameters.full_rate_age('2012-03-01') + birthdate = numpy.array( + ["1950-01-01", "1953-01-01", "1956-01-01", "1959-01-01"], dtype="datetime64[D]" + ) + parameter_at_instant = parameters.full_rate_age("2012-03-01") node = parameter_at_instant.full_rate_age_by_birthdate[birthdate] assert_near(node.year, [65, 66, 67, 67]) assert_near(node.month, [0, 2, 0, 0]) From 2d2c50c27f706254b42af2844785fcb0c8fc04ca Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 18:51:34 +0200 Subject: [PATCH 107/188] chore(version): bump --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a085b4cf..07e0856ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 41.5.1 [#1216](https://github.com/openfisca/openfisca-core/pull/1216) + +#### Technical changes + +- Fix styles by applying `black`. +- Add a `black` dry-run check to `make lint` + ## 41.5.0 [#1212](https://github.com/openfisca/openfisca-core/pull/1212) #### New features diff --git a/setup.py b/setup.py index e66da731b1..6e8f3b5f70 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.5.0", + version="41.5.1", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 5dd0ea13e629ce1c3db36366ba37d663ee199975 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 19:49:34 +0200 Subject: [PATCH 108/188] build(isort): add dry-run check --- openfisca_tasks/lint.mk | 1 + setup.cfg | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index a46b4d7c43..704493b83a 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -11,6 +11,7 @@ check-syntax-errors: . ## Run linters to check for syntax and style errors. check-style: $(shell git ls-files "*.py") @$(call print_help,$@:) + @isort --check $? @black --check $? @flake8 $? @$(call print_pass,$@:) diff --git a/setup.cfg b/setup.cfg index 07fc80f4ed..3ea61f8450 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,13 +27,15 @@ score = no case_sensitive = true force_alphabetical_sort_within_sections = false group_by_package = true +honor_noqa = true include_trailing_comma = true known_first_party = openfisca_core known_openfisca = openfisca_country_template, openfisca_extension_template -known_typing = *abc*, *mypy*, *types*, *typing* +known_typing = *collections.abc*, *typing*, *typing_extensions* +known_types = *types* profile = black py_version = 39 -sections = FUTURE, TYPING, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER +sections = FUTURE, TYPING, TYPES, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER [coverage:paths] source = . */site-packages From e918bbac35790564718421dd2b47fe83275c2d35 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 19:50:20 +0200 Subject: [PATCH 109/188] fix(isort): linting errors --- openfisca_core/commons/formulas.py | 3 ++- openfisca_core/commons/misc.py | 3 ++- openfisca_core/commons/rates.py | 3 ++- openfisca_core/entities/_core_entity.py | 5 +++-- openfisca_core/populations/population.py | 3 ++- openfisca_core/simulations/simulation.py | 3 ++- openfisca_core/simulations/typing.py | 2 +- openfisca_core/taxbenefitsystems/tax_benefit_system.py | 3 ++- openfisca_core/taxscales/amount_tax_scale_like.py | 2 +- openfisca_core/taxscales/rate_tax_scale_like.py | 2 +- openfisca_core/taxscales/tax_scale_like.py | 2 +- openfisca_core/tools/test_runner.py | 3 ++- openfisca_core/types/_domain.py | 3 ++- openfisca_core/variables/variable.py | 3 ++- tests/core/parameters_date_indexing/test_date_indexing.py | 3 ++- 15 files changed, 27 insertions(+), 16 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index b0f4a97b5e..bbcc4fe565 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,6 +1,7 @@ -from openfisca_core.types import Array, ArrayLike from typing import Any, Dict, Sequence, TypeVar +from openfisca_core.types import Array, ArrayLike + import numpy T = TypeVar("T") diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 0966fa1dce..ee985071bf 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,6 +1,7 @@ -from openfisca_core.types import Array from typing import TypeVar +from openfisca_core.types import Array + T = TypeVar("T") diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index 246fdfcd4c..7ede496f8c 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -1,6 +1,7 @@ -from openfisca_core.types import Array, ArrayLike from typing import Optional +from openfisca_core.types import Array, ArrayLike + import numpy diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index a578e656b0..94cba71eaf 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -1,10 +1,11 @@ from __future__ import annotations -from abc import abstractmethod -from openfisca_core.types import TaxBenefitSystem, Variable from typing import Any +from openfisca_core.types import TaxBenefitSystem, Variable + import os +from abc import abstractmethod from .role import Role from .typing import Entity diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index fe1137d83b..06d6803885 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -1,9 +1,10 @@ from __future__ import annotations -from openfisca_core.types import Array, Entity, Period, Role, Simulation from typing import Dict, NamedTuple, Optional, Sequence, Union from typing_extensions import TypedDict +from openfisca_core.types import Array, Entity, Period, Role, Simulation + import traceback import numpy diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 9e7e0034ea..93becda960 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,8 +1,9 @@ from __future__ import annotations -from openfisca_core.types import Population, TaxBenefitSystem, Variable from typing import Dict, Mapping, NamedTuple, Optional, Set +from openfisca_core.types import Population, TaxBenefitSystem, Variable + import tempfile import warnings diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py index 18fa797c4a..8603d0d811 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/typing.py @@ -2,13 +2,13 @@ from __future__ import annotations -from abc import abstractmethod from collections.abc import Iterable, Sequence from numpy.typing import NDArray as Array from typing import Protocol, TypeVar, TypedDict, Union from typing_extensions import NotRequired, Required, TypeAlias import datetime +from abc import abstractmethod from numpy import bool_ as Bool from numpy import datetime64 as Date diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 54a7413d1a..14b607feac 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -1,9 +1,10 @@ from __future__ import annotations import typing -from openfisca_core.types import ParameterNodeAtInstant from typing import Any, Dict, Optional, Sequence, Union +from openfisca_core.types import ParameterNodeAtInstant + import ast import copy import functools diff --git a/openfisca_core/taxscales/amount_tax_scale_like.py b/openfisca_core/taxscales/amount_tax_scale_like.py index 7cdfd4cb34..865ce3200c 100644 --- a/openfisca_core/taxscales/amount_tax_scale_like.py +++ b/openfisca_core/taxscales/amount_tax_scale_like.py @@ -1,6 +1,6 @@ -import abc import typing +import abc import bisect import os diff --git a/openfisca_core/taxscales/rate_tax_scale_like.py b/openfisca_core/taxscales/rate_tax_scale_like.py index b890eb6801..eb8afd872d 100644 --- a/openfisca_core/taxscales/rate_tax_scale_like.py +++ b/openfisca_core/taxscales/rate_tax_scale_like.py @@ -1,8 +1,8 @@ from __future__ import annotations -import abc import typing +import abc import bisect import os diff --git a/openfisca_core/taxscales/tax_scale_like.py b/openfisca_core/taxscales/tax_scale_like.py index c0c1a8adb9..2d64e3afeb 100644 --- a/openfisca_core/taxscales/tax_scale_like.py +++ b/openfisca_core/taxscales/tax_scale_like.py @@ -1,8 +1,8 @@ from __future__ import annotations -import abc import typing +import abc import copy import numpy diff --git a/openfisca_core/tools/test_runner.py b/openfisca_core/tools/test_runner.py index 21987068e1..ea77401c6e 100644 --- a/openfisca_core/tools/test_runner.py +++ b/openfisca_core/tools/test_runner.py @@ -1,9 +1,10 @@ from __future__ import annotations -from openfisca_core.types import TaxBenefitSystem from typing import Any, Dict, Optional, Sequence, Union from typing_extensions import Literal, TypedDict +from openfisca_core.types import TaxBenefitSystem + import dataclasses import os import pathlib diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types/_domain.py index 8b9022d418..d324f1b2cf 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types/_domain.py @@ -1,10 +1,11 @@ from __future__ import annotations -import abc import typing_extensions from typing import Any, Optional from typing_extensions import Protocol +import abc + import numpy diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 2693a31211..e70c0d05d9 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -1,8 +1,9 @@ from __future__ import annotations -from openfisca_core.types import Formula, Instant from typing import Optional, Union +from openfisca_core.types import Formula, Instant + import datetime import re import textwrap diff --git a/tests/core/parameters_date_indexing/test_date_indexing.py b/tests/core/parameters_date_indexing/test_date_indexing.py index 35f5dfd477..05bb770823 100644 --- a/tests/core/parameters_date_indexing/test_date_indexing.py +++ b/tests/core/parameters_date_indexing/test_date_indexing.py @@ -2,10 +2,11 @@ import numpy -from openfisca_core.model_api import * # noqa from openfisca_core.parameters import ParameterNode from openfisca_core.tools import assert_near +from openfisca_core.model_api import * # noqa + LOCAL_DIR = os.path.dirname(os.path.abspath(__file__)) parameters = ParameterNode(directory_path=LOCAL_DIR) From e4ecbaaba8d3339f07f4cab80d751bd3d73d2913 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 19:55:26 +0200 Subject: [PATCH 110/188] chore(version): bump --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07e0856ecd..109e9ac19c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 41.5.2 [#1217](https://github.com/openfisca/openfisca-core/pull/1217) + +#### Technical changes + +- Fix styles by applying `isort`. +- Add a `isort` dry-run check to `make lint` + ### 41.5.1 [#1216](https://github.com/openfisca/openfisca-core/pull/1216) #### Technical changes diff --git a/setup.py b/setup.py index 6e8f3b5f70..15d1613187 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,7 @@ setup( name="OpenFisca-Core", - version="41.5.1", + version="41.5.2", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 5bad465fe16bb0f553d2c2fe6cfb36c885ff045f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 20:04:13 +0200 Subject: [PATCH 111/188] build(flake8): fix doc linting config --- setup.cfg | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3ea61f8450..5c1d03bb80 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,19 +8,25 @@ # W503/504: We break lines before binary operators (Knuth's style). [flake8] +convention = google +docstring_style = google extend-ignore = D ignore = E203, E501, F405, RST301, W503 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors openfisca_core/types +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods max-line-length = 88 -per-file-ignores = */typing.py:D101,D102,E704, */__init__.py:F401 +per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, mod, obj strictness = short +[pylint.MASTER] +load-plugins = pylint_per_file_ignores + [pylint.message_control] disable = all enable = C0115,C0116,R0401 +per-file-ignores = /tests/:C0116 score = no [isort] From c0d433da96b6080ebc9892eb09439e1de579634f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 20:06:47 +0200 Subject: [PATCH 112/188] build(pylint): add lib to skip checks per file --- setup.py | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/setup.py b/setup.py index 15d1613187..b7e5841d98 100644 --- a/setup.py +++ b/setup.py @@ -40,28 +40,29 @@ ] api_requirements = [ - "Flask >=2.2.3, < 3.0", - "Flask-Cors >=3.0.10, < 4.0", - "gunicorn >=21.0, < 22.0", - "Werkzeug >=2.2.3, < 3.0", + "Flask >=2.2.3, <3.0", + "Flask-Cors >=3.0.10, <4.0", + "gunicorn >=21.0, <22.0", + "Werkzeug >=2.2.3, <3.0", ] dev_requirements = [ - "black >=23.1.0, < 24.0", - "coverage >=6.5.0, < 7.0", - "darglint >=1.8.1, < 2.0", - "flake8 >=6.0.0, < 7.0.0", - "flake8-bugbear >=23.3.23, < 24.0", - "flake8-docstrings >=1.7.0, < 2.0", - "flake8-print >=5.0.0, < 6.0", - "flake8-rst-docstrings >=0.3.0, < 0.4.0", - "idna >=3.4, < 4.0", - "isort >=5.12.0, < 6.0", - "mypy >=1.1.1, < 2.0", - "openapi-spec-validator >=0.7.1, < 0.8.0", - "pycodestyle >=2.10.0, < 3.0", - "pylint >=2.17.1, < 3.0", - "xdoctest >=1.1.1, < 2.0", + "black >=23.1.0, <24.0", + "coverage >=6.5.0, <7.0", + "darglint >=1.8.1, <2.0", + "flake8 >=6.0.0, <7.0.0", + "flake8-bugbear >=23.3.23, <24.0", + "flake8-docstrings >=1.7.0, <2.0", + "flake8-print >=5.0.0, <6.0", + "flake8-rst-docstrings >=0.3.0, <0.4.0", + "idna >=3.4, <4.0", + "isort >=5.12.0, <6.0", + "mypy >=1.1.1, <2.0", + "openapi-spec-validator >=0.7.1, <0.8.0", + "pycodestyle >=2.10.0, <3.0", + "pylint >=2.17.1, <3.0", + "pylint-per-file-ignores >=1.3.2, <2.0", + "xdoctest >=1.1.1, <2.0", ] + api_requirements setup( @@ -102,12 +103,12 @@ "web-api": api_requirements, "dev": dev_requirements, "ci": [ - "build >=0.10.0, < 0.11.0", - "coveralls >=3.3.1, < 4.0", - "twine >=5.1.1, < 6.0", - "wheel >=0.40.0, < 0.41.0", + "build >=0.10.0, <0.11.0", + "coveralls >=3.3.1, <4.0", + "twine >=5.1.1, <6.0", + "wheel >=0.40.0, <0.41.0", ], - "tracker": ["OpenFisca-Tracker >=0.4.0, < 0.5.0"], + "tracker": ["OpenFisca-Tracker >=0.4.0, <0.5.0"], }, include_package_data=True, # Will read MANIFEST.in install_requires=general_requirements, From 7f6c88755fb74f10d276e341e69f63b07a71d7d5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 20:28:12 +0200 Subject: [PATCH 113/188] fix(entities): doc & doctest --- openfisca_core/entities/_core_entity.py | 3 ++ openfisca_core/entities/helpers.py | 56 ++++++++++++++++++++++++- openfisca_tasks/lint.mk | 1 + 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index 94cba71eaf..c84844ac5f 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -36,6 +36,9 @@ class _CoreEntity: def __init__(self, key: str, plural: str, label: str, doc: str, *args: Any) -> None: ... + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.key})" + def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystem) -> None: self._tax_benefit_system = tax_benefit_system diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index ca2e0b33a7..26c841c19d 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -17,8 +17,62 @@ def build_entity( class_override=None, containing_entities=(), ): + """Build a SingleEntity or a GroupEntity. + + Args: + key: Key to identify the :class:`.Entity`. or :class:`.GroupEntity`. + plural: ``key``, pluralised. + label: A summary description. + doc: A full description. + roles: A list of :class:`.Role`, if it's a :class:`.GroupEntity`. + is_person: If is an individual, or not. + class_override: ? + containing_entities: Keys of contained entities. + + Returns: + :obj:`.Entity` or :obj:`.GroupEntity`: + :obj:`.Entity`: When ``is_person`` is True. + :obj:`.GroupEntity`: When ``is_person`` is False. + + Raises: + ValueError if ``roles`` is not a sequence. + + Examples: + >>> from openfisca_core import entities + + >>> build_entity( + ... "syndicate", + ... "syndicates", + ... "Banks loaning jointly.", + ... roles = [], + ... containing_entities = (), + ... ) + GroupEntity(syndicate) + + >>> build_entity( + ... "company", + ... "companies", + ... "A small or medium company.", + ... is_person = True, + ... ) + Entity(company) + + >>> role = entities.Role({"key": "key"}, object()) + + >>> build_entity( + ... "syndicate", + ... "syndicates", + ... "Banks loaning jointly.", + ... roles = role, + ... ) + Traceback (most recent call last): + TypeError: 'Role' object is not iterable + + """ + if is_person: return Entity(key, plural, label, doc) + else: return GroupEntity( key, plural, label, doc, roles, containing_entities=containing_entities @@ -31,7 +85,7 @@ def find_role( """Find a Role in a GroupEntity. Args: - group_entity (GroupEntity): The entity to search in. + roles (Iterable[Role]): The roles to search. key (str): The key of the role to find. Defaults to `None`. total (int | None): The `max` attribute of the role to find. diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 704493b83a..324104f7c0 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -19,6 +19,7 @@ check-style: $(shell git ls-files "*.py") ## Run linters to check for syntax and style errors in the doc. lint-doc: \ lint-doc-commons \ + lint-doc-entities \ lint-doc-types \ ; From c5b8b264f756376c2bf7154c8e631788c4995fdd Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 20:42:07 +0200 Subject: [PATCH 114/188] fix(entities): ignore linting for types --- openfisca_core/entities/__init__.py | 4 ++-- openfisca_core/entities/_core_entity.py | 6 +++++- openfisca_core/entities/helpers.py | 4 ++-- openfisca_core/entities/role.py | 2 +- openfisca_core/entities/{typing.py => types.py} | 0 openfisca_core/projectors/helpers.py | 3 ++- openfisca_core/projectors/typing.py | 3 ++- setup.cfg | 6 ++++-- 8 files changed, 18 insertions(+), 10 deletions(-) rename openfisca_core/entities/{typing.py => types.py} (100%) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index 2ba382b7d8..a1cd397a3a 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -21,10 +21,10 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from . import typing +from . import types from .entity import Entity from .group_entity import GroupEntity from .helpers import build_entity, find_role from .role import Role -__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "find_role", "typing"] +__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "find_role", "types"] diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index c84844ac5f..ecd17becae 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -8,7 +8,7 @@ from abc import abstractmethod from .role import Role -from .typing import Entity +from .types import Entity class _CoreEntity: @@ -40,6 +40,7 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key})" def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystem) -> None: + """An Entity belongs to a TaxBenefitSystem.""" self._tax_benefit_system = tax_benefit_system def get_variable( @@ -47,9 +48,11 @@ def get_variable( variable_name: str, check_existence: bool = False, ) -> Variable | None: + """Get a ``variable_name`` from ``variables``.""" return self._tax_benefit_system.get_variable(variable_name, check_existence) def check_variable_defined_for_entity(self, variable_name: str) -> None: + """Check if ``variable_name`` is defined for ``self``.""" variable: Variable | None entity: Entity @@ -69,5 +72,6 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None: raise ValueError(os.linesep.join(message)) def check_role_validity(self, role: Any) -> None: + """Check if a ``role`` is an instance of Role.""" if role is not None and not isinstance(role, Role): raise ValueError(f"{role} is not a valid role") diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 26c841c19d..000c8028cd 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -4,7 +4,7 @@ from .entity import Entity from .group_entity import GroupEntity -from .typing import Role +from .types import Role def build_entity( @@ -93,7 +93,7 @@ def find_role( Role | None: The role if found, else `None`. Examples: - >>> from openfisca_core.entities.typing import RoleParams + >>> from openfisca_core.entities.types import RoleParams >>> principal = RoleParams( ... key="principal", diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 6bae50bc25..a4cb75a860 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -6,7 +6,7 @@ import dataclasses import textwrap -from .typing import Entity +from .types import Entity class Role: diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/types.py similarity index 100% rename from openfisca_core/entities/typing.py rename to openfisca_core/entities/types.py diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index b99ac2884f..ce94f9773e 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -1,7 +1,8 @@ from __future__ import annotations from collections.abc import Mapping -from openfisca_core.entities.typing import Entity, GroupEntity, Role + +from openfisca_core.entities.types import Entity, GroupEntity, Role from openfisca_core import entities, projectors diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py index 5dbbd90e07..a6ce8e3987 100644 --- a/openfisca_core/projectors/typing.py +++ b/openfisca_core/projectors/typing.py @@ -1,9 +1,10 @@ from __future__ import annotations from collections.abc import Mapping -from openfisca_core.entities.typing import Entity, GroupEntity from typing import Protocol +from openfisca_core.entities.types import Entity, GroupEntity + class Population(Protocol): @property diff --git a/setup.cfg b/setup.cfg index 5c1d03bb80..8b76a452aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,8 +25,10 @@ load-plugins = pylint_per_file_ignores [pylint.message_control] disable = all -enable = C0115,C0116,R0401 -per-file-ignores = /tests/:C0116 +enable = C0115, C0116, R0401 +per-file-ignores = + types.py:C0115,C0116 + /tests/:C0116 score = no [isort] From 9eb8e250e6c4c41e60424755f4e239b61b77815a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 21:00:39 +0200 Subject: [PATCH 115/188] chore(build): add missing doctest paths --- openfisca_tasks/test_code.mk | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index 2734d20275..d84956ea5c 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -31,7 +31,7 @@ test-code: test-core test-country test-extension ## Run openfisca-core tests. test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 -d ":") @$(call print_help,$@:) - @pytest --quiet --capture=no --xdoctest --xdoctest-verbose=0 \ + @pytest --capture=no --xdoctest --xdoctest-verbose=0 \ openfisca_core/commons \ openfisca_core/entities \ openfisca_core/holders \ diff --git a/setup.cfg b/setup.cfg index 8b76a452aa..9673496d71 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ docstring_style = google extend-ignore = D ignore = E203, E501, F405, RST301, W503 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors openfisca_core/types max-line-length = 88 per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged From ae3dd85dc96eacf628820413bb0cd8165cc404e3 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 21:03:01 +0200 Subject: [PATCH 116/188] chore(version): bump --- CHANGELOG.md | 9 +++++++++ setup.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 109e9ac19c..75fdd9ed3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +### 41.5.3 [#1218](https://github.com/openfisca/openfisca-core/pull/1218) + +#### Technical changes + +- Fix `flake8` doc linting: + - Add format "google" + - Fix per-file skips +- Fix failing lints + ### 41.5.2 [#1217](https://github.com/openfisca/openfisca-core/pull/1217) #### Technical changes diff --git a/setup.py b/setup.py index b7e5841d98..e822e138bc 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.5.2", + version="41.5.3", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 077a4af48b0fd17332baf046f2fbf97d7adc8ec6 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 21:54:24 +0200 Subject: [PATCH 117/188] chore(types): re-order types per module --- openfisca_core/{types/_domain.py => types.py} | 130 ++++++++++-------- openfisca_core/types/__init__.py | 83 ----------- openfisca_core/types/_data.py | 54 -------- 3 files changed, 76 insertions(+), 191 deletions(-) rename openfisca_core/{types/_domain.py => types.py} (66%) delete mode 100644 openfisca_core/types/__init__.py delete mode 100644 openfisca_core/types/_data.py diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types.py similarity index 66% rename from openfisca_core/types/_domain.py rename to openfisca_core/types.py index d324f1b2cf..ede4ba9bce 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types.py @@ -1,134 +1,138 @@ from __future__ import annotations import typing_extensions -from typing import Any, Optional -from typing_extensions import Protocol +from collections.abc import Sequence +from numpy.typing import NDArray +from typing import Any, TypeVar +from typing_extensions import Protocol, TypeAlias import abc import numpy +N = TypeVar("N", bound=numpy.generic, covariant=True) -class Entity(Protocol): - """Entity protocol.""" +#: Type representing an numpy array. +Array: TypeAlias = NDArray[N] + +L = TypeVar("L") + +#: Type representing an array-like object. +ArrayLike: TypeAlias = Sequence[L] + +#: Type variable representing an error. +E = TypeVar("E", covariant=True) + +#: Type variable representing a value. +A = TypeVar("A", covariant=True) + +# Entities + + +class Entity(Protocol): key: Any plural: Any @abc.abstractmethod def check_role_validity(self, role: Any) -> None: - """Abstract method.""" + ... @abc.abstractmethod def check_variable_defined_for_entity(self, variable_name: Any) -> None: - """Abstract method.""" + ... @abc.abstractmethod def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Optional[Any]: - """Abstract method.""" + ) -> Any | None: + ... -class Formula(Protocol): - """Formula protocol.""" +class Role(Protocol): + entity: Any + subroles: Any - @abc.abstractmethod - def __call__( - self, - population: Population, - instant: Instant, - params: Params, - ) -> numpy.ndarray: - """Abstract method.""" +# Holders -class Holder(Protocol): - """Holder protocol.""" +class Holder(Protocol): @abc.abstractmethod def clone(self, population: Any) -> Holder: - """Abstract method.""" + ... @abc.abstractmethod def get_memory_usage(self) -> Any: - """Abstract method.""" + ... -class Instant(Protocol): - """Instant protocol.""" +# Parameters @typing_extensions.runtime_checkable class ParameterNodeAtInstant(Protocol): - """ParameterNodeAtInstant protocol.""" + ... -class Params(Protocol): - """Params protocol.""" +# Periods - @abc.abstractmethod - def __call__(self, instant: Instant) -> ParameterNodeAtInstant: - """Abstract method.""" + +class Instant(Protocol): + ... @typing_extensions.runtime_checkable class Period(Protocol): - """Period protocol.""" - @property @abc.abstractmethod def start(self) -> Any: - """Abstract method.""" + ... @property @abc.abstractmethod def unit(self) -> Any: - """Abstract method.""" + ... -class Population(Protocol): - """Population protocol.""" +# Populations + +class Population(Protocol): entity: Any @abc.abstractmethod def get_holder(self, variable_name: Any) -> Any: - """Abstract method.""" - + ... -class Role(Protocol): - """Role protocol.""" - entity: Any - subroles: Any +# Simulations class Simulation(Protocol): - """Simulation protocol.""" - @abc.abstractmethod def calculate(self, variable_name: Any, period: Any) -> Any: - """Abstract method.""" + ... @abc.abstractmethod def calculate_add(self, variable_name: Any, period: Any) -> Any: - """Abstract method.""" + ... @abc.abstractmethod def calculate_divide(self, variable_name: Any, period: Any) -> Any: - """Abstract method.""" + ... @abc.abstractmethod - def get_population(self, plural: Optional[Any]) -> Any: - """Abstract method.""" + def get_population(self, plural: Any | None) -> Any: + ... -class TaxBenefitSystem(Protocol): - """TaxBenefitSystem protocol.""" +# Tax-Benefit systems + +class TaxBenefitSystem(Protocol): person_entity: Any @abc.abstractmethod @@ -136,11 +140,29 @@ def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Optional[Any]: + ) -> Any | None: """Abstract method.""" -class Variable(Protocol): - """Variable protocol.""" +# Variables + +class Variable(Protocol): entity: Any + + +class Formula(Protocol): + @abc.abstractmethod + def __call__( + self, + population: Population, + instant: Instant, + params: Params, + ) -> Array[Any]: + ... + + +class Params(Protocol): + @abc.abstractmethod + def __call__(self, instant: Instant) -> ParameterNodeAtInstant: + ... diff --git a/openfisca_core/types/__init__.py b/openfisca_core/types/__init__.py deleted file mode 100644 index eb403c46c9..0000000000 --- a/openfisca_core/types/__init__.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Data types and protocols used by OpenFisca Core. - -The type definitions included in this sub-package are intended for -contributors, to help them better understand and document contracts -and expected behaviours. - -Official Public API: - * :attr:`.Array` - * ``ArrayLike`` - * :attr:`.Cache` - * :attr:`.Entity` - * :attr:`.Formula` - * :attr:`.Holder` - * :attr:`.Instant` - * :attr:`.ParameterNodeAtInstant` - * :attr:`.Params` - * :attr:`.Period` - * :attr:`.Population` - * :attr:`.Role`, - * :attr:`.Simulation`, - * :attr:`.TaxBenefitSystem` - * :attr:`.Variable` - -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 import Array, ArrayLike # noqa: F401 -from ._domain import ( # noqa: F401 - Entity, - Formula, - Holder, - Instant, - ParameterNodeAtInstant, - Params, - Period, - Population, - Role, - Simulation, - TaxBenefitSystem, - Variable, -) - -__all__ = [ - "Array", - "ArrayLike", - "Entity", - "Formula", - "Holder", - "Instant", - "ParameterNodeAtInstant", - "Params", - "Period", - "Population", - "Role", - "Simulation", - "TaxBenefitSystem", - "Variable", -] diff --git a/openfisca_core/types/_data.py b/openfisca_core/types/_data.py deleted file mode 100644 index 928e1b9174..0000000000 --- a/openfisca_core/types/_data.py +++ /dev/null @@ -1,54 +0,0 @@ -# from typing import Sequence, TypeVar, Union -# from nptyping import types, NDArray as Array -from numpy.typing import NDArray as Array # noqa: F401 -from typing import Sequence, TypeVar - -# import numpy - -# NumpyT = TypeVar("NumpyT", numpy.bytes_, numpy.number, numpy.object_, numpy.str_) -T = TypeVar("T", bool, bytes, float, int, object, str) - -# types._ndarray_meta._Type = Union[type, numpy.dtype, TypeVar] - -# ArrayLike = Union[Array[T], Sequence[T]] -ArrayLike = 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 a694d281fe0115eb67ce7095487b3f44e253933a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 22:08:35 +0200 Subject: [PATCH 118/188] fix(types): commons --- openfisca_core/commons/formulas.py | 20 ++++++++++---------- openfisca_core/commons/misc.py | 4 ++-- openfisca_core/commons/rates.py | 14 +++++++------- openfisca_tasks/lint.mk | 4 +++- setup.cfg | 9 ++++++--- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index bbcc4fe565..1cb35a571e 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,17 +1,15 @@ -from typing import Any, Dict, Sequence, TypeVar +from typing import Any, Dict, Union from openfisca_core.types import Array, ArrayLike import numpy -T = TypeVar("T") - def apply_thresholds( - input: Array[float], + input: Array[numpy.float_], thresholds: ArrayLike[float], choices: ArrayLike[float], -) -> Array[float]: +) -> Array[numpy.float_]: """Makes a choice based on an input and thresholds. From a list of ``choices``, this function selects one of these values @@ -40,7 +38,7 @@ def apply_thresholds( """ - condlist: Sequence[Array[bool]] + condlist: list[Union[Array[numpy.bool_], bool]] condlist = [input <= threshold for threshold in thresholds] if len(condlist) == len(choices) - 1: @@ -58,7 +56,9 @@ def apply_thresholds( return numpy.select(condlist, choices) -def concat(this: ArrayLike[str], that: ArrayLike[str]) -> Array[str]: +def concat( + this: Union[Array[Any], ArrayLike[str]], that: Union[Array[Any], ArrayLike[str]] +) -> Array[numpy.str_]: """Concatenates the values of two arrays. Args: @@ -88,8 +88,8 @@ def concat(this: ArrayLike[str], that: ArrayLike[str]) -> Array[str]: def switch( conditions: Array[Any], - value_by_condition: Dict[float, T], -) -> Array[T]: + value_by_condition: Dict[float, Any], +) -> Array[Any]: """Mimicks a switch statement. Given an array of conditions, returns an array of the same size, @@ -120,4 +120,4 @@ def switch( condlist = [conditions == condition for condition in value_by_condition.keys()] - return numpy.select(condlist, value_by_condition.values()) + return numpy.select(condlist, tuple(value_by_condition.values())) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index ee985071bf..9c7fcc68fd 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,4 +1,4 @@ -from typing import TypeVar +from typing import Any, TypeVar, Union from openfisca_core.types import Array @@ -43,7 +43,7 @@ def empty_clone(original: T) -> T: return new -def stringify_array(array: Array) -> str: +def stringify_array(array: Union[Array[Any], None]) -> str: """Generates a clean string representation of a numpy array. Args: diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index 7ede496f8c..6df1f1fee2 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -6,10 +6,10 @@ def average_rate( - target: Array[float], + target: Array[numpy.float_], varying: ArrayLike[float], trim: Optional[ArrayLike[float]] = None, -) -> Array[float]: +) -> Array[numpy.float_]: """Computes the average rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -41,7 +41,7 @@ def average_rate( """ - average_rate: Array[float] + average_rate: Array[numpy.float_] average_rate = 1 - target / varying @@ -62,10 +62,10 @@ def average_rate( def marginal_rate( - target: Array[float], - varying: Array[float], + target: Array[numpy.float_], + varying: Array[numpy.float_], trim: Optional[ArrayLike[float]] = None, -) -> Array[float]: +) -> Array[numpy.float_]: """Computes the marginal rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -97,7 +97,7 @@ def marginal_rate( """ - marginal_rate: Array[float] + marginal_rate: Array[numpy.float_] marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 324104f7c0..828c518008 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -39,7 +39,9 @@ lint-doc-%: ## Run static type checkers for type errors. check-types: @$(call print_help,$@:) - @mypy openfisca_core/entities openfisca_core/projectors + @mypy \ + openfisca_core/commons \ + openfisca_core/types.py @$(call print_pass,$@:) ## Run code formatters to correct style errors. diff --git a/setup.cfg b/setup.cfg index 9673496d71..d54f6263d2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,9 +65,12 @@ python_files = **/*.py testpaths = tests [mypy] -ignore_missing_imports = True -install_types = True -non_interactive = True +disallow_any_unimported = true +ignore_missing_imports = true +install_types = true +non_interactive = true +plugins = numpy.typing.mypy_plugin +python_version = 3.9 [mypy-openfisca_core.commons.tests.*] ignore_errors = True From 69d9a32e076edd2aceec6cc4584dea0dc1762ba9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 01:23:24 +0200 Subject: [PATCH 119/188] build(make): fix make test --- openfisca_tasks/test_code.mk | 3 +-- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index d84956ea5c..723c94ece0 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -36,8 +36,7 @@ test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 openfisca_core/entities \ openfisca_core/holders \ openfisca_core/periods \ - openfisca_core/projectors \ - openfisca_core/types + openfisca_core/projectors @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ coverage run -m \ ${openfisca} test $? \ diff --git a/setup.cfg b/setup.cfg index d54f6263d2..e266be625c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,7 @@ docstring_style = google extend-ignore = D ignore = E203, E501, F405, RST301, W503 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors openfisca_core/types +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors max-line-length = 88 per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged From e185a1d9e67acf181757e48076f574968ad54023 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 01:45:18 +0200 Subject: [PATCH 120/188] chore(commons): remove outdated Dict --- openfisca_core/commons/formulas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 1cb35a571e..c1cc159e50 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Union +from typing import Any, Union from openfisca_core.types import Array, ArrayLike @@ -88,7 +88,7 @@ def concat( def switch( conditions: Array[Any], - value_by_condition: Dict[float, Any], + value_by_condition: dict[float, Any], ) -> Array[Any]: """Mimicks a switch statement. From 32b61f2f1e4a9b6ac1f764cd9399bff0ae469970 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 03:35:52 +0200 Subject: [PATCH 121/188] refactor(types): avoid any --- openfisca_core/commons/formulas.py | 14 ++++++++------ openfisca_core/commons/misc.py | 6 ++++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index c1cc159e50..bb4b95b5ae 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,4 +1,5 @@ -from typing import Any, Union +from collections.abc import Mapping +from typing import Union from openfisca_core.types import Array, ArrayLike @@ -57,7 +58,8 @@ def apply_thresholds( def concat( - this: Union[Array[Any], ArrayLike[str]], that: Union[Array[Any], ArrayLike[str]] + this: Union[Array[numpy.str_], ArrayLike[str]], + that: Union[Array[numpy.str_], ArrayLike[str]], ) -> Array[numpy.str_]: """Concatenates the values of two arrays. @@ -66,7 +68,7 @@ def concat( that: Another array to concatenate. Returns: - :obj:`numpy.ndarray` of :obj:`float`: + :obj:`numpy.ndarray` of :obj:`numpy.str_`: An array with the concatenated values. Examples: @@ -87,9 +89,9 @@ def concat( def switch( - conditions: Array[Any], - value_by_condition: dict[float, Any], -) -> Array[Any]: + conditions: Array[numpy.float_], + value_by_condition: Mapping[float, float], +) -> Array[numpy.float_]: """Mimicks a switch statement. Given an array of conditions, returns an array of the same size, diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 9c7fcc68fd..ff1c2e3864 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,7 +1,9 @@ -from typing import Any, TypeVar, Union +from typing import Optional, TypeVar from openfisca_core.types import Array +import numpy + T = TypeVar("T") @@ -43,7 +45,7 @@ def empty_clone(original: T) -> T: return new -def stringify_array(array: Union[Array[Any], None]) -> str: +def stringify_array(array: Optional[Array[numpy.generic]]) -> str: """Generates a clean string representation of a numpy array. Args: From d835de6791b98b4084901792224c20b7a6be0a15 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 05:14:37 +0200 Subject: [PATCH 122/188] doc(commons): add py.typed --- openfisca_core/commons/formulas.py | 24 ++++++++++++------------ openfisca_core/commons/misc.py | 6 +++--- openfisca_core/commons/py.typed | 0 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 openfisca_core/commons/py.typed diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index bb4b95b5ae..bce9206938 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,16 +1,16 @@ from collections.abc import Mapping from typing import Union -from openfisca_core.types import Array, ArrayLike - import numpy +from openfisca_core import types as t + def apply_thresholds( - input: Array[numpy.float_], - thresholds: ArrayLike[float], - choices: ArrayLike[float], -) -> Array[numpy.float_]: + input: t.Array[numpy.float_], + thresholds: t.ArrayLike[float], + choices: t.ArrayLike[float], +) -> t.Array[numpy.float_]: """Makes a choice based on an input and thresholds. From a list of ``choices``, this function selects one of these values @@ -39,7 +39,7 @@ def apply_thresholds( """ - condlist: list[Union[Array[numpy.bool_], bool]] + condlist: list[Union[t.Array[numpy.bool_], bool]] condlist = [input <= threshold for threshold in thresholds] if len(condlist) == len(choices) - 1: @@ -58,9 +58,9 @@ def apply_thresholds( def concat( - this: Union[Array[numpy.str_], ArrayLike[str]], - that: Union[Array[numpy.str_], ArrayLike[str]], -) -> Array[numpy.str_]: + this: Union[t.Array[numpy.str_], t.ArrayLike[str]], + that: Union[t.Array[numpy.str_], t.ArrayLike[str]], +) -> t.Array[numpy.str_]: """Concatenates the values of two arrays. Args: @@ -89,9 +89,9 @@ def concat( def switch( - conditions: Array[numpy.float_], + conditions: t.Array[numpy.float_], value_by_condition: Mapping[float, float], -) -> Array[numpy.float_]: +) -> t.Array[numpy.float_]: """Mimicks a switch statement. Given an array of conditions, returns an array of the same size, diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index ff1c2e3864..342bbbe5fb 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,9 +1,9 @@ from typing import Optional, TypeVar -from openfisca_core.types import Array - import numpy +from openfisca_core import types as t + T = TypeVar("T") @@ -45,7 +45,7 @@ def empty_clone(original: T) -> T: return new -def stringify_array(array: Optional[Array[numpy.generic]]) -> str: +def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str: """Generates a clean string representation of a numpy array. Args: diff --git a/openfisca_core/commons/py.typed b/openfisca_core/commons/py.typed new file mode 100644 index 0000000000..e69de29bb2 From 5aaca8690e3034a3083c4dea0b11fa7a066c1930 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 01:13:34 +0200 Subject: [PATCH 123/188] chore(version): bump --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75fdd9ed3f..4b0f3e63a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.5.4 [#1219](https://github.com/openfisca/openfisca-core/pull/1219) + +#### Technical changes + +- Fix doc & type definitions in the commons module + ### 41.5.3 [#1218](https://github.com/openfisca/openfisca-core/pull/1218) #### Technical changes diff --git a/setup.py b/setup.py index e822e138bc..92971f40bc 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.5.3", + version="41.5.4", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From f9225240538857cb1605ebe5fcb71e8cac29f19d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 15 Sep 2024 22:43:38 +0200 Subject: [PATCH 124/188] fix(types): entities --- openfisca_core/entities/__init__.py | 12 +++++++++++- openfisca_core/entities/_core_entity.py | 8 ++++++-- openfisca_core/entities/role.py | 6 +++--- openfisca_core/entities/types.py | 15 ++++++++++++--- openfisca_core/populations/population.py | 6 +++--- openfisca_core/projectors/helpers.py | 4 ++-- openfisca_core/projectors/typing.py | 4 ++-- openfisca_core/types.py | 10 +++++++++- openfisca_tasks/lint.mk | 1 + setup.cfg | 13 ++----------- 10 files changed, 51 insertions(+), 28 deletions(-) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index a1cd397a3a..9546773cb8 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -27,4 +27,14 @@ from .helpers import build_entity, find_role from .role import Role -__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "find_role", "types"] +SingleEntity = Entity + +__all__ = [ + "Entity", + "SingleEntity", + "GroupEntity", + "Role", + "build_entity", + "find_role", + "types", +] diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index ecd17becae..89bd28eaf3 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -8,7 +8,7 @@ from abc import abstractmethod from .role import Role -from .types import Entity +from .types import CoreEntity class _CoreEntity: @@ -49,12 +49,16 @@ def get_variable( check_existence: bool = False, ) -> Variable | None: """Get a ``variable_name`` from ``variables``.""" + if self._tax_benefit_system is None: + raise ValueError( + "You must set 'tax_benefit_system' before calling this method." + ) return self._tax_benefit_system.get_variable(variable_name, check_existence) def check_variable_defined_for_entity(self, variable_name: str) -> None: """Check if ``variable_name`` is defined for ``self``.""" variable: Variable | None - entity: Entity + entity: CoreEntity variable = self.get_variable(variable_name, check_existence=True) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index a4cb75a860..d703578160 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -6,7 +6,7 @@ import dataclasses import textwrap -from .types import Entity +from .types import SingleEntity class Role: @@ -48,7 +48,7 @@ class Role: """ #: The Entity the Role belongs to. - entity: Entity + entity: SingleEntity #: A description of the Role. description: _Description @@ -79,7 +79,7 @@ def doc(self) -> str | None: """A full description, non-indented.""" return self.description.doc - def __init__(self, description: Mapping[str, Any], entity: Entity) -> None: + def __init__(self, description: Mapping[str, Any], entity: SingleEntity) -> None: self.description = _Description( **{ key: value diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index 1a2fb06b2c..5fe994a182 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -3,16 +3,25 @@ from collections.abc import Iterable from typing import Protocol, TypedDict +from openfisca_core import types -class Entity(Protocol): +# Entities + + +class CoreEntity(types.CoreEntity, Protocol): ... -class GroupEntity(Protocol): +class SingleEntity(types.SingleEntity, Protocol): + key: str + plural: str | None + + +class GroupEntity(types.GroupEntity, Protocol): ... -class Role(Protocol): +class Role(types.Role, Protocol): max: int | None subroles: Iterable[Role] | None diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index 06d6803885..e3ef6b209a 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -3,7 +3,7 @@ from typing import Dict, NamedTuple, Optional, Sequence, Union from typing_extensions import TypedDict -from openfisca_core.types import Array, Entity, Period, Role, Simulation +from openfisca_core.types import Array, Period, Role, Simulation, SingleEntity import traceback @@ -16,12 +16,12 @@ class Population: simulation: Optional[Simulation] - entity: Entity + entity: SingleEntity _holders: Dict[str, holders.Holder] count: int ids: Array[str] - def __init__(self, entity: Entity) -> None: + def __init__(self, entity: SingleEntity) -> None: self.simulation = None self.entity = entity self._holders = {} diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index ce94f9773e..b3b7e6f2d3 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -2,7 +2,7 @@ from collections.abc import Mapping -from openfisca_core.entities.types import Entity, GroupEntity, Role +from openfisca_core.types import GroupEntity, Role, SingleEntity from openfisca_core import entities, projectors @@ -110,7 +110,7 @@ def get_projector_from_shortcut( """ - entity: Entity | GroupEntity = population.entity + entity: SingleEntity | GroupEntity = population.entity if isinstance(entity, entities.Entity): populations: Mapping[ diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py index a6ce8e3987..186f90e30c 100644 --- a/openfisca_core/projectors/typing.py +++ b/openfisca_core/projectors/typing.py @@ -3,12 +3,12 @@ from collections.abc import Mapping from typing import Protocol -from openfisca_core.entities.types import Entity, GroupEntity +from openfisca_core.types import GroupEntity, SingleEntity class Population(Protocol): @property - def entity(self) -> Entity: + def entity(self) -> SingleEntity: ... @property diff --git a/openfisca_core/types.py b/openfisca_core/types.py index ede4ba9bce..8b83bff1c2 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -30,7 +30,7 @@ # Entities -class Entity(Protocol): +class CoreEntity(Protocol): key: Any plural: Any @@ -51,6 +51,14 @@ def get_variable( ... +class SingleEntity(CoreEntity, Protocol): + ... + + +class GroupEntity(CoreEntity, Protocol): + ... + + class Role(Protocol): entity: Any subroles: Any diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 828c518008..445abba10b 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -41,6 +41,7 @@ check-types: @$(call print_help,$@:) @mypy \ openfisca_core/commons \ + openfisca_core/entities \ openfisca_core/types.py @$(call print_pass,$@:) diff --git a/setup.cfg b/setup.cfg index e266be625c..451f68b911 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,7 +59,7 @@ skip_covered = true skip_empty = true [tool:pytest] -addopts = --doctest-modules --disable-pytest-warnings --showlocals +addopts = --disable-pytest-warnings --doctest-modules --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py testpaths = tests @@ -72,14 +72,5 @@ non_interactive = true plugins = numpy.typing.mypy_plugin python_version = 3.9 -[mypy-openfisca_core.commons.tests.*] -ignore_errors = True - -[mypy-openfisca_core.holders.tests.*] -ignore_errors = True - -[mypy-openfisca_core.periods.tests.*] -ignore_errors = True - -[mypy-openfisca_core.scripts.*] +[mypy-openfisca_core.*.tests.*] ignore_errors = True From 7f53201a62e5b01b09ee18d4da06b0d9fbadb6ac Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 04:50:32 +0200 Subject: [PATCH 125/188] fix(entities): types bis --- openfisca_core/entities/_core_entity.py | 27 +++++++------- openfisca_core/entities/entity.py | 5 +-- openfisca_core/entities/group_entity.py | 10 +++--- openfisca_core/entities/helpers.py | 33 +++++++++-------- openfisca_core/entities/types.py | 47 +++++++++++++++++++------ 5 files changed, 75 insertions(+), 47 deletions(-) diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index 89bd28eaf3..9a2707d19d 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -1,24 +1,19 @@ from __future__ import annotations -from typing import Any - -from openfisca_core.types import TaxBenefitSystem, Variable - import os from abc import abstractmethod +from . import types as t from .role import Role -from .types import CoreEntity class _CoreEntity: """Base class to build entities from.""" #: A key to identify the entity. - key: str - + key: t.EntityKey #: The ``key``, pluralised. - plural: str | None + plural: t.EntityPlural | None #: A summary description. label: str | None @@ -30,16 +25,18 @@ class _CoreEntity: is_person: bool #: A TaxBenefitSystem instance. - _tax_benefit_system: TaxBenefitSystem | None = None + _tax_benefit_system: t.TaxBenefitSystem | None = None @abstractmethod - def __init__(self, key: str, plural: str, label: str, doc: str, *args: Any) -> None: + def __init__( + self, key: str, plural: str, label: str, doc: str, *args: object + ) -> None: ... def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key})" - def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystem) -> None: + def set_tax_benefit_system(self, tax_benefit_system: t.TaxBenefitSystem) -> None: """An Entity belongs to a TaxBenefitSystem.""" self._tax_benefit_system = tax_benefit_system @@ -47,7 +44,7 @@ def get_variable( self, variable_name: str, check_existence: bool = False, - ) -> Variable | None: + ) -> t.Variable | None: """Get a ``variable_name`` from ``variables``.""" if self._tax_benefit_system is None: raise ValueError( @@ -57,8 +54,8 @@ def get_variable( def check_variable_defined_for_entity(self, variable_name: str) -> None: """Check if ``variable_name`` is defined for ``self``.""" - variable: Variable | None - entity: CoreEntity + variable: t.Variable | None + entity: t.CoreEntity variable = self.get_variable(variable_name, check_existence=True) @@ -75,7 +72,7 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None: ) raise ValueError(os.linesep.join(message)) - def check_role_validity(self, role: Any) -> None: + def check_role_validity(self, role: object) -> None: """Check if a ``role`` is an instance of Role.""" if role is not None and not isinstance(role, Role): raise ValueError(f"{role} is not a valid role") diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index ea2deb505d..8194772663 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,5 +1,6 @@ import textwrap +from . import types as t from ._core_entity import _CoreEntity @@ -9,8 +10,8 @@ class Entity(_CoreEntity): """ def __init__(self, key: str, plural: str, label: str, doc: str) -> None: - self.key = key + self.key = t.EntityKey(key) self.label = label - self.plural = plural + self.plural = t.EntityPlural(plural) self.doc = textwrap.dedent(doc) self.is_person = True diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index eeae52d38f..d2242983d6 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,11 +1,11 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping -from typing import Any +from collections.abc import Iterable, Sequence import textwrap from itertools import chain +from . import types as t from ._core_entity import _CoreEntity from .role import Role @@ -34,12 +34,12 @@ def __init__( plural: str, label: str, doc: str, - roles: Iterable[Mapping[str, Any]], + roles: Sequence[t.RoleParams], containing_entities: Iterable[str] = (), ) -> None: - self.key = key + self.key = t.EntityKey(key) self.label = label - self.plural = plural + self.plural = t.EntityPlural(plural) self.doc = textwrap.dedent(doc) self.is_person = False self.roles_description = roles diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 000c8028cd..90b9ffd948 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,22 +1,23 @@ from __future__ import annotations -from typing import Iterable +from collections.abc import Iterable, Sequence +from typing import Optional +from . import types as t from .entity import Entity from .group_entity import GroupEntity -from .types import Role def build_entity( - key, - plural, - label, - doc="", - roles=None, - is_person=False, - class_override=None, - containing_entities=(), -): + key: str, + plural: str, + label: str, + doc: str = "", + roles: Optional[Sequence[t.RoleParams]] = None, + is_person: bool = False, + class_override: object | None = None, + containing_entities: Sequence[str] = (), +) -> t.SingleEntity | t.GroupEntity: """Build a SingleEntity or a GroupEntity. Args: @@ -35,7 +36,7 @@ def build_entity( :obj:`.GroupEntity`: When ``is_person`` is False. Raises: - ValueError if ``roles`` is not a sequence. + NotImplementedError: if ``roles`` is None. Examples: >>> from openfisca_core import entities @@ -73,15 +74,17 @@ def build_entity( if is_person: return Entity(key, plural, label, doc) - else: + if roles is not None: return GroupEntity( key, plural, label, doc, roles, containing_entities=containing_entities ) + raise NotImplementedError + def find_role( - roles: Iterable[Role], key: str, *, total: int | None = None -) -> Role | None: + roles: Iterable[t.Role], key: t.RoleKey, *, total: int | None = None +) -> t.Role | None: """Find a Role in a GroupEntity. Args: diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index 5fe994a182..de011d8ee8 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -1,27 +1,40 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Protocol, TypedDict +from typing import NewType, Protocol +from typing_extensions import Required, TypedDict -from openfisca_core import types +from openfisca_core import types as t # Entities +#: For example "person". +EntityKey = NewType("EntityKey", str) + +#: For example "persons". +EntityPlural = NewType("EntityPlural", str) + +#: For example "principal". +RoleKey = NewType("RoleKey", str) + +#: For example "parents". +RolePlural = NewType("RolePlural", str) -class CoreEntity(types.CoreEntity, Protocol): - ... +class CoreEntity(t.CoreEntity, Protocol): + key: EntityKey + plural: EntityPlural | None -class SingleEntity(types.SingleEntity, Protocol): - key: str - plural: str | None + +class SingleEntity(t.SingleEntity, Protocol): + ... -class GroupEntity(types.GroupEntity, Protocol): +class GroupEntity(t.GroupEntity, Protocol): ... -class Role(types.Role, Protocol): +class Role(t.Role, Protocol): max: int | None subroles: Iterable[Role] | None @@ -31,9 +44,23 @@ def key(self) -> str: class RoleParams(TypedDict, total=False): - key: str + key: Required[str] plural: str label: str doc: str max: int subroles: list[str] + + +# Tax-Benefit systems + + +class TaxBenefitSystem(t.TaxBenefitSystem, Protocol): + ... + + +# Variables + + +class Variable(t.Variable, Protocol): + ... From 7b37709ae223e4765145deba5efc68df117b6f74 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 01:18:28 +0200 Subject: [PATCH 126/188] chore(version): bump --- CHANGELOG.md | 6 ++ openfisca_core/entities/types.py | 5 -- openfisca_core/types.py | 5 ++ setup.cfg | 116 +++++++++++++++++-------------- setup.py | 2 +- 5 files changed, 75 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b0f3e63a0..ade8610231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.5.5 [#1220](https://github.com/openfisca/openfisca-core/pull/1220) + +#### Technical changes + +- Fix doc & type definitions in the entities module + ### 41.5.4 [#1219](https://github.com/openfisca/openfisca-core/pull/1219) #### Technical changes diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index de011d8ee8..2f9acd0402 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -35,13 +35,8 @@ class GroupEntity(t.GroupEntity, Protocol): class Role(t.Role, Protocol): - max: int | None subroles: Iterable[Role] | None - @property - def key(self) -> str: - ... - class RoleParams(TypedDict, total=False): key: Required[str] diff --git a/openfisca_core/types.py b/openfisca_core/types.py index 8b83bff1c2..b34a555434 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -61,8 +61,13 @@ class GroupEntity(CoreEntity, Protocol): class Role(Protocol): entity: Any + max: int | None subroles: Any + @property + def key(self) -> str: + ... + # Holders diff --git a/setup.cfg b/setup.cfg index 451f68b911..cc850c06a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,76 +1,86 @@ -# C011X: We (progressively) document the code base. -# D10X: We (progressively) check docstrings (see https://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping). -# DARXXX: We (progressively) check docstrings (see https://github.com/terrencepreilly/darglint#error-codes). -# E203: We ignore a false positive in whitespace before ":" (see https://github.com/PyCQA/pycodestyle/issues/373). -# F403/405: We ignore * imports. -# R0401: We avoid cyclic imports —required for unit/doc tests. -# RST301: We use Google Python Style (see https://pypi.org/project/flake8-rst-docstrings/). -# W503/504: We break lines before binary operators (Knuth's style). +# C011X: We (progressively) document the code base. +# D10X: We (progressively) check docstrings (see https://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping). +# DARXXX: We (progressively) check docstrings (see https://github.com/terrencepreilly/darglint#error-codes). +# E203: We ignore a false positive in whitespace before ":" (see https://github.com/PyCQA/pycodestyle/issues/373). +# F403/405: We ignore * imports. +# R0401: We avoid cyclic imports —required for unit/doc tests. +# RST301: We use Google Python Style (see https://pypi.org/project/flake8-rst-docstrings/). +# W503/504: We break lines before binary operators (Knuth's style). [flake8] -convention = google -docstring_style = google -extend-ignore = D -ignore = E203, E501, F405, RST301, W503 -in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors -max-line-length = 88 -per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 -rst-directives = attribute, deprecated, seealso, versionadded, versionchanged -rst-roles = any, attr, class, exc, func, meth, mod, obj -strictness = short +convention = google +docstring_style = google +extend-ignore = D +ignore = E203, E501, F405, RST301, W503 +in-place = true +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors +max-line-length = 88 +per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 +rst-directives = attribute, deprecated, seealso, versionadded, versionchanged +rst-roles = any, attr, class, exc, func, meth, mod, obj +strictness = short [pylint.MASTER] -load-plugins = pylint_per_file_ignores +load-plugins = pylint_per_file_ignores [pylint.message_control] -disable = all -enable = C0115, C0116, R0401 -per-file-ignores = +disable = all +enable = C0115, C0116, R0401 +per-file-ignores = types.py:C0115,C0116 /tests/:C0116 -score = no +score = no [isort] -case_sensitive = true +case_sensitive = true force_alphabetical_sort_within_sections = false -group_by_package = true -honor_noqa = true -include_trailing_comma = true -known_first_party = openfisca_core -known_openfisca = openfisca_country_template, openfisca_extension_template -known_typing = *collections.abc*, *typing*, *typing_extensions* -known_types = *types* -profile = black -py_version = 39 -sections = FUTURE, TYPING, TYPES, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER +group_by_package = true +honor_noqa = true +include_trailing_comma = true +known_first_party = openfisca_core +known_openfisca = openfisca_country_template, openfisca_extension_template +known_typing = *collections.abc*, *typing*, *typing_extensions* +known_types = *types* +profile = black +py_version = 39 +sections = FUTURE, TYPING, TYPES, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER [coverage:paths] -source = . */site-packages +source = . */site-packages [coverage:run] -branch = true -source = openfisca_core, openfisca_web_api +branch = true +source = openfisca_core, openfisca_web_api [coverage:report] -fail_under = 75 -show_missing = true -skip_covered = true -skip_empty = true +fail_under = 75 +show_missing = true +skip_covered = true +skip_empty = true [tool:pytest] -addopts = --disable-pytest-warnings --doctest-modules --showlocals -doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE -python_files = **/*.py -testpaths = tests +addopts = --disable-pytest-warnings --doctest-modules --showlocals +doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE +python_files = **/*.py +testpaths = tests [mypy] -disallow_any_unimported = true -ignore_missing_imports = true -install_types = true -non_interactive = true -plugins = numpy.typing.mypy_plugin -python_version = 3.9 +check_untyped_defs = false +disallow_any_decorated = false +disallow_any_explicit = false +disallow_any_expr = false +disallow_any_unimported = false +follow_imports = skip +ignore_missing_imports = true +implicit_reexport = false +install_types = true +non_interactive = true +plugins = numpy.typing.mypy_plugin +pretty = true +python_version = 3.9 +strict = false +warn_no_return = true +warn_unreachable = true [mypy-openfisca_core.*.tests.*] -ignore_errors = True +ignore_errors = True diff --git a/setup.py b/setup.py index 92971f40bc..cca107bee8 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.5.4", + version="41.5.5", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 2f8072fdf270bfd204f1103b227a893f80a3bdaa Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 26 Jun 2023 13:29:13 +0200 Subject: [PATCH 127/188] Remove pre Python 3.9 syntax --- .github/get_pypi_info.py | 15 +- openfisca_core/commons/__init__.py | 8 +- openfisca_core/commons/dummy.py | 1 - openfisca_core/commons/formulas.py | 22 +- openfisca_core/commons/misc.py | 4 +- openfisca_core/commons/rates.py | 20 +- openfisca_core/commons/tests/test_dummy.py | 3 +- openfisca_core/commons/tests/test_formulas.py | 21 +- openfisca_core/commons/tests/test_rates.py | 6 +- .../data_storage/in_memory_storage.py | 30 +-- .../data_storage/on_disk_storage.py | 19 +- openfisca_core/entities/_core_entity.py | 22 +- openfisca_core/entities/entity.py | 4 +- openfisca_core/entities/group_entity.py | 9 +- openfisca_core/entities/helpers.py | 39 +-- openfisca_core/entities/role.py | 12 +- openfisca_core/entities/tests/test_entity.py | 1 - .../entities/tests/test_group_entity.py | 11 +- openfisca_core/entities/tests/test_role.py | 1 - openfisca_core/entities/types.py | 18 +- openfisca_core/errors/__init__.py | 14 +- openfisca_core/errors/cycle_error.py | 2 - openfisca_core/errors/empty_argument_error.py | 2 +- openfisca_core/errors/nan_creation_error.py | 2 - .../errors/parameter_not_found_error.py | 19 +- .../errors/parameter_parsing_error.py | 13 +- .../errors/period_mismatch_error.py | 6 +- .../errors/situation_parsing_error.py | 14 +- openfisca_core/errors/spiral_error.py | 2 - .../errors/variable_name_config_error.py | 6 +- .../errors/variable_not_found_error.py | 25 +- openfisca_core/experimental/memory_config.py | 10 +- openfisca_core/holders/helpers.py | 21 +- openfisca_core/holders/holder.py | 113 ++++---- openfisca_core/holders/tests/test_helpers.py | 113 ++++---- openfisca_core/indexed_enums/enum.py | 17 +- openfisca_core/indexed_enums/enum_array.py | 32 +-- openfisca_core/model_api.py | 13 +- openfisca_core/parameters/__init__.py | 9 +- openfisca_core/parameters/at_instant_like.py | 7 +- openfisca_core/parameters/config.py | 11 +- openfisca_core/parameters/helpers.py | 48 ++-- openfisca_core/parameters/parameter.py | 70 ++--- .../parameters/parameter_at_instant.py | 37 +-- openfisca_core/parameters/parameter_node.py | 75 +++--- .../parameters/parameter_node_at_instant.py | 24 +- openfisca_core/parameters/parameter_scale.py | 57 ++-- .../parameters/parameter_scale_bracket.py | 6 +- ...ial_asof_date_parameter_node_at_instant.py | 37 +-- .../vectorial_parameter_node_at_instant.py | 112 ++++---- openfisca_core/periods/_parsers.py | 15 +- openfisca_core/periods/config.py | 4 +- openfisca_core/periods/date_unit.py | 6 +- openfisca_core/periods/helpers.py | 14 +- openfisca_core/periods/instant_.py | 68 ++--- openfisca_core/periods/period_.py | 167 +++++++----- .../periods/tests/helpers/test_helpers.py | 54 ++-- .../periods/tests/helpers/test_instant.py | 106 ++++---- .../periods/tests/helpers/test_period.py | 214 +++++++-------- openfisca_core/periods/tests/test__parsers.py | 86 +++--- openfisca_core/periods/tests/test_instant.py | 44 ++-- openfisca_core/periods/tests/test_period.py | 218 +++++++-------- .../populations/group_population.py | 104 ++++---- openfisca_core/populations/population.py | 111 ++++---- .../projectors/entity_to_person_projector.py | 2 +- .../first_person_to_entity_projector.py | 2 +- openfisca_core/projectors/helpers.py | 23 +- openfisca_core/projectors/projector.py | 7 +- openfisca_core/projectors/typing.py | 23 +- .../unique_role_to_entity_projector.py | 2 +- openfisca_core/reforms/reform.py | 36 ++- openfisca_core/scripts/__init__.py | 29 +- openfisca_core/scripts/find_placeholders.py | 26 +- .../measure_numpy_condition_notations.py | 41 ++- .../scripts/measure_performances.py | 84 +++--- .../measure_performances_fancy_indexing.py | 29 +- .../xml_to_yaml_country_template.py | 10 +- .../xml_to_yaml_extension_template.py | 9 +- .../scripts/migrations/v24_to_25.py | 37 ++- openfisca_core/scripts/openfisca_command.py | 10 +- openfisca_core/scripts/remove_fuzzy.py | 2 +- openfisca_core/scripts/run_test.py | 11 +- .../scripts/simulation_generator.py | 47 ++-- openfisca_core/simulations/__init__.py | 12 +- .../simulations/_build_default_simulation.py | 9 +- .../simulations/_build_from_variables.py | 9 +- openfisca_core/simulations/_type_guards.py | 97 ++++--- openfisca_core/simulations/helpers.py | 21 +- openfisca_core/simulations/simulation.py | 243 ++++++++--------- .../simulations/simulation_builder.py | 249 ++++++++++-------- openfisca_core/simulations/typing.py | 24 +- .../taxbenefitsystems/tax_benefit_system.py | 187 +++++++------ .../taxscales/abstract_rate_tax_scale.py | 10 +- .../taxscales/abstract_tax_scale.py | 16 +- .../taxscales/amount_tax_scale_like.py | 9 +- openfisca_core/taxscales/helpers.py | 4 +- .../linear_average_rate_tax_scale.py | 4 +- .../taxscales/marginal_amount_tax_scale.py | 13 +- .../taxscales/marginal_rate_tax_scale.py | 56 ++-- .../taxscales/rate_tax_scale_like.py | 38 ++- .../taxscales/single_amount_tax_scale.py | 11 +- openfisca_core/taxscales/tax_scale_like.py | 30 +-- openfisca_core/tools/__init__.py | 45 ++-- openfisca_core/tools/simulation_dumper.py | 63 ++--- openfisca_core/tools/test_runner.py | 109 ++++---- openfisca_core/tracers/computation_log.py | 31 ++- openfisca_core/tracers/flat_trace.py | 20 +- openfisca_core/tracers/full_tracer.py | 17 +- openfisca_core/tracers/performance_log.py | 21 +- openfisca_core/tracers/simple_tracer.py | 6 +- openfisca_core/tracers/trace_node.py | 8 +- .../tracing_parameter_node_at_instant.py | 18 +- openfisca_core/types.py | 57 ++-- openfisca_core/variables/helpers.py | 27 +- .../variables/tests/test_definition_period.py | 12 +- openfisca_core/variables/variable.py | 160 +++++------ openfisca_core/warnings/libyaml_warning.py | 6 +- openfisca_core/warnings/memory_warning.py | 6 +- openfisca_core/warnings/tempfile_warning.py | 6 +- openfisca_web_api/app.py | 31 ++- openfisca_web_api/errors.py | 9 +- openfisca_web_api/handlers.py | 51 ++-- openfisca_web_api/loader/__init__.py | 3 - openfisca_web_api/loader/entities.py | 8 +- openfisca_web_api/loader/parameters.py | 17 +- openfisca_web_api/loader/spec.py | 70 ++--- .../loader/tax_benefit_system.py | 8 +- openfisca_web_api/loader/variables.py | 21 +- openfisca_web_api/scripts/serve.py | 19 +- pyproject.toml | 9 + setup.cfg | 11 +- setup.py | 24 +- .../test_parameter_clone.py | 10 +- .../test_parameter_validation.py | 12 +- .../test_date_indexing.py | 10 +- .../test_fancy_indexing.py | 84 +++--- .../test_abstract_rate_tax_scale.py | 2 +- .../tax_scales/test_abstract_tax_scale.py | 2 +- .../test_linear_average_rate_tax_scale.py | 14 +- .../test_marginal_amount_tax_scale.py | 6 +- .../test_marginal_rate_tax_scale.py | 38 +-- .../tax_scales/test_rate_tax_scale_like.py | 2 +- .../test_single_amount_tax_scale.py | 12 +- .../tax_scales/test_tax_scales_commons.py | 6 +- tests/core/test_axes.py | 114 ++++---- tests/core/test_calculate_output.py | 14 +- tests/core/test_countries.py | 30 +-- tests/core/test_cycles.py | 50 ++-- tests/core/test_dump_restore.py | 14 +- tests/core/test_entities.py | 83 +++--- tests/core/test_extensions.py | 8 +- tests/core/test_formulas.py | 48 ++-- tests/core/test_holders.py | 66 +++-- tests/core/test_opt_out_cache.py | 20 +- tests/core/test_parameters.py | 37 +-- tests/core/test_projectors.py | 86 +++--- tests/core/test_reforms.py | 125 +++++---- tests/core/test_simulation_builder.py | 225 ++++++++++------ tests/core/test_simulations.py | 10 +- tests/core/test_tracers.py | 103 ++++---- tests/core/test_yaml.py | 39 +-- tests/core/tools/test_assert_near.py | 6 +- .../tools/test_runner/test_yaml_runner.py | 42 +-- tests/core/variables/test_annualize.py | 22 +- .../core/variables/test_definition_period.py | 12 +- tests/core/variables/test_variables.py | 145 +++++----- tests/fixtures/appclient.py | 4 +- tests/fixtures/entities.py | 12 +- tests/fixtures/extensions.py | 10 +- tests/fixtures/simulations.py | 4 +- tests/fixtures/variables.py | 2 +- .../case_with_extension/test_extensions.py | 14 +- .../web_api/case_with_reform/test_reforms.py | 14 +- tests/web_api/loader/test_parameters.py | 34 ++- tests/web_api/test_calculate.py | 107 ++++---- tests/web_api/test_entities.py | 6 +- tests/web_api/test_headers.py | 4 +- tests/web_api/test_helpers.py | 8 +- tests/web_api/test_parameters.py | 36 +-- tests/web_api/test_spec.py | 38 ++- tests/web_api/test_trace.py | 50 ++-- tests/web_api/test_variables.py | 55 ++-- 182 files changed, 3408 insertions(+), 3246 deletions(-) diff --git a/.github/get_pypi_info.py b/.github/get_pypi_info.py index 03d2f1ab15..fd7a2c9238 100644 --- a/.github/get_pypi_info.py +++ b/.github/get_pypi_info.py @@ -18,12 +18,14 @@ def get_info(package_name: str = "") -> dict: ::return:: A dict with last_version, url and sha256 """ if package_name == "": - raise ValueError("Package name not provided.") + msg = "Package name not provided." + raise ValueError(msg) url = f"https://pypi.org/pypi/{package_name}/json" print(f"Calling {url}") # noqa: T201 resp = requests.get(url) if resp.status_code != 200: - raise Exception(f"ERROR calling PyPI ({url}) : {resp}") + msg = f"ERROR calling PyPI ({url}) : {resp}" + raise Exception(msg) resp = resp.json() version = resp["info"]["version"] @@ -38,19 +40,19 @@ def get_info(package_name: str = "") -> dict: return {} -def replace_in_file(filepath: str, info: dict): +def replace_in_file(filepath: str, info: dict) -> None: """Replace placeholder in meta.yaml by their values. ::filepath:: Path to meta.yaml, with filename. ::info:: Dict with information to populate. """ - with open(filepath, "rt", encoding="utf-8") as fin: + with open(filepath, encoding="utf-8") as fin: meta = fin.read() # Replace with info from PyPi meta = meta.replace("PYPI_VERSION", info["last_version"]) meta = meta.replace("PYPI_URL", info["url"]) meta = meta.replace("PYPI_SHA256", info["sha256"]) - with open(filepath, "wt", encoding="utf-8") as fout: + with open(filepath, "w", encoding="utf-8") as fout: fout.write(meta) print(f"File {filepath} has been updated with info from PyPi.") # noqa: T201 @@ -75,6 +77,7 @@ def replace_in_file(filepath: str, info: dict): args = parser.parse_args() info = get_info(args.package) print( # noqa: T201 - "Information of the last published PyPi package :", info["last_version"] + "Information of the last published PyPi package :", + info["last_version"], ) replace_in_file(args.filename, info) diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index b3b5d8cbb2..807abec778 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -52,9 +52,9 @@ # Official Public API -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 +from .formulas import apply_thresholds, concat, switch +from .misc import empty_clone, stringify_array +from .rates import average_rate, marginal_rate __all__ = ["apply_thresholds", "concat", "switch"] __all__ = ["empty_clone", "stringify_array", *__all__] @@ -62,6 +62,6 @@ # Deprecated -from .dummy import Dummy # noqa: F401 +from .dummy import Dummy __all__ = ["Dummy", *__all__] diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 3788e48705..5135a8f555 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -20,4 +20,3 @@ def __init__(self) -> None: "and will be removed in the future.", ] warnings.warn(" ".join(message), DeprecationWarning, stacklevel=2) - pass diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index bce9206938..909c4cd14a 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -7,10 +7,10 @@ def apply_thresholds( - input: t.Array[numpy.float_], + input: t.Array[numpy.float64], thresholds: t.ArrayLike[float], choices: t.ArrayLike[float], -) -> t.Array[numpy.float_]: +) -> t.Array[numpy.float64]: """Makes a choice based on an input and thresholds. From a list of ``choices``, this function selects one of these values @@ -38,7 +38,6 @@ def apply_thresholds( array([10, 10, 15, 15, 20]) """ - condlist: list[Union[t.Array[numpy.bool_], bool]] condlist = [input <= threshold for threshold in thresholds] @@ -47,12 +46,9 @@ def apply_thresholds( # 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.", - ] - ) + assert len(condlist) == len( + choices + ), "'apply_thresholds' must be called with the same number of thresholds than choices, or one more choice." return numpy.select(condlist, choices) @@ -78,7 +74,6 @@ def concat( array(['this1.0', 'that2.5']...) """ - if isinstance(this, numpy.ndarray) and not numpy.issubdtype(this.dtype, numpy.str_): this = this.astype("str") @@ -89,9 +84,9 @@ def concat( def switch( - conditions: t.Array[numpy.float_], + conditions: t.Array[numpy.float64], value_by_condition: Mapping[float, float], -) -> t.Array[numpy.float_]: +) -> t.Array[numpy.float64]: """Mimicks a switch statement. Given an array of conditions, returns an array of the same size, @@ -115,11 +110,10 @@ def switch( 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()] + condlist = [conditions == condition for condition in value_by_condition] return numpy.select(condlist, tuple(value_by_condition.values())) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 342bbbe5fb..3c9cd5feab 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -30,7 +30,6 @@ def empty_clone(original: T) -> T: True """ - Dummy: object new: T @@ -60,7 +59,7 @@ def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str: >>> stringify_array(None) 'None' - >>> array = numpy.array([10, 20.]) + >>> array = numpy.array([10, 20.0]) >>> stringify_array(array) '[10.0, 20.0]' @@ -73,7 +72,6 @@ def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str: "[, {}, Array[numpy.float_]: +) -> Array[numpy.float64]: """Computes the average rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -35,13 +35,12 @@ def average_rate( Examples: >>> target = numpy.array([1, 2, 3]) >>> varying = [2, 2, 2] - >>> trim = [-1, .25] + >>> trim = [-1, 0.25] >>> average_rate(target, varying, trim) array([ nan, 0. , -0.5]) """ - - average_rate: Array[numpy.float_] + average_rate: Array[numpy.float64] average_rate = 1 - target / varying @@ -62,10 +61,10 @@ def average_rate( def marginal_rate( - target: Array[numpy.float_], - varying: Array[numpy.float_], + target: Array[numpy.float64], + varying: Array[numpy.float64], trim: Optional[ArrayLike[float]] = None, -) -> Array[numpy.float_]: +) -> Array[numpy.float64]: """Computes the marginal rate of a target net income. Given a ``target`` net income, and according to the ``varying`` gross @@ -91,13 +90,12 @@ def marginal_rate( Examples: >>> target = numpy.array([1, 2, 3]) >>> varying = numpy.array([1, 2, 4]) - >>> trim = [.25, .75] + >>> trim = [0.25, 0.75] >>> marginal_rate(target, varying, trim) array([nan, 0.5]) """ - - marginal_rate: Array[numpy.float_] + marginal_rate: Array[numpy.float64] marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) diff --git a/openfisca_core/commons/tests/test_dummy.py b/openfisca_core/commons/tests/test_dummy.py index d4ecec3842..4dd13eabab 100644 --- a/openfisca_core/commons/tests/test_dummy.py +++ b/openfisca_core/commons/tests/test_dummy.py @@ -3,8 +3,7 @@ from openfisca_core.commons import Dummy -def test_dummy_deprecation(): +def test_dummy_deprecation() -> None: """Dummy throws a deprecation warning when instantiated.""" - with pytest.warns(DeprecationWarning): assert Dummy() diff --git a/openfisca_core/commons/tests/test_formulas.py b/openfisca_core/commons/tests/test_formulas.py index 82755583e6..91866bd0c0 100644 --- a/openfisca_core/commons/tests/test_formulas.py +++ b/openfisca_core/commons/tests/test_formulas.py @@ -5,9 +5,8 @@ from openfisca_core import commons -def test_apply_thresholds_when_several_inputs(): +def test_apply_thresholds_when_several_inputs() -> None: """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] @@ -17,9 +16,8 @@ def test_apply_thresholds_when_several_inputs(): assert_array_equal(result, [10, 10, 15, 15, 20, 20, 25]) -def test_apply_thresholds_when_too_many_thresholds(): +def test_apply_thresholds_when_too_many_thresholds() -> None: """Raises an AssertionError when thresholds > choices.""" - input_ = numpy.array([6]) thresholds = [5, 7, 9, 11] choices = [10, 15, 20] @@ -28,9 +26,8 @@ def test_apply_thresholds_when_too_many_thresholds(): assert commons.apply_thresholds(input_, thresholds, choices) -def test_apply_thresholds_when_too_many_choices(): +def test_apply_thresholds_when_too_many_choices() -> None: """Raises an AssertionError when thresholds < choices - 1.""" - input_ = numpy.array([6]) thresholds = [5, 7] choices = [10, 15, 20, 25] @@ -39,9 +36,8 @@ def test_apply_thresholds_when_too_many_choices(): assert commons.apply_thresholds(input_, thresholds, choices) -def test_concat_when_this_is_array_not_str(): +def test_concat_when_this_is_array_not_str() -> None: """Casts ``this`` to ``str`` when it is a NumPy array other than string.""" - this = numpy.array([1, 2]) that = numpy.array(["la", "o"]) @@ -50,9 +46,8 @@ def test_concat_when_this_is_array_not_str(): assert_array_equal(result, ["1la", "2o"]) -def test_concat_when_that_is_array_not_str(): +def test_concat_when_that_is_array_not_str() -> None: """Casts ``that`` to ``str`` when it is a NumPy array other than string.""" - this = numpy.array(["ho", "cha"]) that = numpy.array([1, 2]) @@ -61,9 +56,8 @@ def test_concat_when_that_is_array_not_str(): assert_array_equal(result, ["ho1", "cha2"]) -def test_concat_when_args_not_str_array_like(): +def test_concat_when_args_not_str_array_like() -> None: """Raises a TypeError when args are not a string array-like object.""" - this = (1, 2) that = (3, 4) @@ -71,9 +65,8 @@ def test_concat_when_args_not_str_array_like(): commons.concat(this, that) -def test_switch_when_values_are_empty(): +def test_switch_when_values_are_empty() -> None: """Raises an AssertionError when the values are empty.""" - conditions = [1, 1, 1, 2] value_by_condition = {} diff --git a/openfisca_core/commons/tests/test_rates.py b/openfisca_core/commons/tests/test_rates.py index 01565d9527..54e24b8d0f 100644 --- a/openfisca_core/commons/tests/test_rates.py +++ b/openfisca_core/commons/tests/test_rates.py @@ -4,9 +4,8 @@ from openfisca_core import commons -def test_average_rate_when_varying_is_zero(): +def test_average_rate_when_varying_is_zero() -> None: """Yields infinity when the varying gross income crosses zero.""" - target = numpy.array([1, 2, 3]) varying = [0, 0, 0] @@ -15,9 +14,8 @@ def test_average_rate_when_varying_is_zero(): assert_array_equal(result, [-numpy.inf, -numpy.inf, -numpy.inf]) -def test_marginal_rate_when_varying_is_zero(): +def test_marginal_rate_when_varying_is_zero() -> None: """Yields infinity when the varying gross income crosses zero.""" - target = numpy.array([1, 2, 3]) varying = numpy.array([0, 0, 0]) diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py index 8fb472046b..0808612ba8 100644 --- a/openfisca_core/data_storage/in_memory_storage.py +++ b/openfisca_core/data_storage/in_memory_storage.py @@ -5,11 +5,9 @@ class InMemoryStorage: - """ - Low-level class responsible for storing and retrieving calculated vectors in memory - """ + """Low-level class responsible for storing and retrieving calculated vectors in memory.""" - def __init__(self, is_eternal=False): + def __init__(self, is_eternal=False) -> None: self._arrays = {} self.is_eternal = is_eternal @@ -23,14 +21,14 @@ def get(self, period): return None return values - def put(self, value, period): + def put(self, value, period) -> None: if self.is_eternal: period = periods.period(DateUnit.ETERNITY) period = periods.period(period) self._arrays[period] = value - def delete(self, period=None): + def delete(self, period=None) -> None: if period is None: self._arrays = {} return @@ -50,16 +48,16 @@ def get_known_periods(self): def get_memory_usage(self): if not self._arrays: - return dict( - nb_arrays=0, - total_nb_bytes=0, - cell_size=numpy.nan, - ) + return { + "nb_arrays": 0, + "total_nb_bytes": 0, + "cell_size": numpy.nan, + } nb_arrays = len(self._arrays) array = next(iter(self._arrays.values())) - return dict( - nb_arrays=nb_arrays, - total_nb_bytes=array.nbytes * nb_arrays, - cell_size=array.itemsize, - ) + return { + "nb_arrays": nb_arrays, + "total_nb_bytes": array.nbytes * nb_arrays, + "cell_size": array.itemsize, + } diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py index dbf8a4eb13..9133db2376 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -9,11 +9,11 @@ class OnDiskStorage: - """ - Low-level class responsible for storing and retrieving calculated vectors on disk - """ + """Low-level class responsible for storing and retrieving calculated vectors on disk.""" - def __init__(self, storage_dir, is_eternal=False, preserve_storage_dir=False): + def __init__( + self, storage_dir, is_eternal=False, preserve_storage_dir=False + ) -> None: self._files = {} self._enums = {} self.is_eternal = is_eternal @@ -24,8 +24,7 @@ def _decode_file(self, file): enum = self._enums.get(file) if enum is not None: return EnumArray(numpy.load(file), enum) - else: - return numpy.load(file) + return numpy.load(file) def get(self, period): if self.is_eternal: @@ -37,7 +36,7 @@ def get(self, period): return None return self._decode_file(values) - def put(self, value, period): + def put(self, value, period) -> None: if self.is_eternal: period = periods.period(DateUnit.ETERNITY) period = periods.period(period) @@ -50,7 +49,7 @@ def put(self, value, period): numpy.save(path, value) self._files[period] = path - def delete(self, period=None): + def delete(self, period=None) -> None: if period is None: self._files = {} return @@ -69,7 +68,7 @@ def delete(self, period=None): def get_known_periods(self): return self._files.keys() - def restore(self): + def restore(self) -> None: self._files = files = {} # Restore self._files from content of storage_dir. for filename in os.listdir(self.storage_dir): @@ -80,7 +79,7 @@ def restore(self): period = periods.period(filename_core) files[period] = path - def __del__(self): + def __del__(self) -> None: if self.preserve_storage_dir: return shutil.rmtree(self.storage_dir) # Remove the holder temporary files diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index 9a2707d19d..b0647fe50a 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -1,11 +1,15 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import os from abc import abstractmethod -from . import types as t from .role import Role +if TYPE_CHECKING: + from . import types as t + class _CoreEntity: """Base class to build entities from.""" @@ -29,9 +33,13 @@ class _CoreEntity: @abstractmethod def __init__( - self, key: str, plural: str, label: str, doc: str, *args: object - ) -> None: - ... + self, + key: str, + plural: str, + label: str, + doc: str, + *args: object, + ) -> None: ... def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key})" @@ -47,8 +55,9 @@ def get_variable( ) -> t.Variable | None: """Get a ``variable_name`` from ``variables``.""" if self._tax_benefit_system is None: + msg = "You must set 'tax_benefit_system' before calling this method." raise ValueError( - "You must set 'tax_benefit_system' before calling this method." + msg, ) return self._tax_benefit_system.get_variable(variable_name, check_existence) @@ -75,4 +84,5 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None: def check_role_validity(self, role: object) -> None: """Check if a ``role`` is an instance of Role.""" if role is not None and not isinstance(role, Role): - raise ValueError(f"{role} is not a valid role") + msg = f"{role} is not a valid role" + raise ValueError(msg) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 8194772663..a3fbaddac3 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -5,9 +5,7 @@ class Entity(_CoreEntity): - """ - Represents an entity (e.g. a person, a household, etc.) on which calculations can be run. - """ + """Represents an entity (e.g. a person, a household, etc.) on which calculations can be run.""" def __init__(self, key: str, plural: str, label: str, doc: str) -> None: self.key = t.EntityKey(key) diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index d2242983d6..a25352ba55 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Iterable, Sequence +from typing import TYPE_CHECKING import textwrap from itertools import chain @@ -9,6 +9,9 @@ from ._core_entity import _CoreEntity from .role import Role +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + class GroupEntity(_CoreEntity): """Represents an entity containing several others with different roles. @@ -26,7 +29,7 @@ class GroupEntity(_CoreEntity): containing_entities: The list of keys of group entities whose members are guaranteed to be a superset of this group's entities. - """ # noqa RST301 + """ def __init__( self, @@ -56,7 +59,7 @@ def __init__( role.subroles = (*role.subroles, subrole) role.max = len(role.subroles) self.flattened_roles = tuple( - chain.from_iterable(role.subroles or [role] for role in self.roles) + chain.from_iterable(role.subroles or [role] for role in self.roles), ) self.is_person = False diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 90b9ffd948..1091b9fb74 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,19 +1,22 @@ from __future__ import annotations -from collections.abc import Iterable, Sequence -from typing import Optional +from typing import TYPE_CHECKING -from . import types as t from .entity import Entity from .group_entity import GroupEntity +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + + from . import types as t + def build_entity( key: str, plural: str, label: str, doc: str = "", - roles: Optional[Sequence[t.RoleParams]] = None, + roles: Sequence[t.RoleParams] | None = None, is_person: bool = False, class_override: object | None = None, containing_entities: Sequence[str] = (), @@ -45,17 +48,17 @@ def build_entity( ... "syndicate", ... "syndicates", ... "Banks loaning jointly.", - ... roles = [], - ... containing_entities = (), - ... ) + ... roles=[], + ... containing_entities=(), + ... ) GroupEntity(syndicate) >>> build_entity( ... "company", ... "companies", ... "A small or medium company.", - ... is_person = True, - ... ) + ... is_person=True, + ... ) Entity(company) >>> role = entities.Role({"key": "key"}, object()) @@ -64,26 +67,33 @@ def build_entity( ... "syndicate", ... "syndicates", ... "Banks loaning jointly.", - ... roles = role, - ... ) + ... roles=role, + ... ) Traceback (most recent call last): TypeError: 'Role' object is not iterable """ - if is_person: return Entity(key, plural, label, doc) if roles is not None: return GroupEntity( - key, plural, label, doc, roles, containing_entities=containing_entities + key, + plural, + label, + doc, + roles, + containing_entities=containing_entities, ) raise NotImplementedError def find_role( - roles: Iterable[t.Role], key: t.RoleKey, *, total: int | None = None + roles: Iterable[t.Role], + key: t.RoleKey, + *, + total: int | None = None, ) -> t.Role | None: """Find a Role in a GroupEntity. @@ -141,7 +151,6 @@ def find_role( Role(first_parent) """ - for role in roles: if role.subroles: for subrole in role.subroles: diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index d703578160..9f28dc7aa8 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,12 +1,14 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping -from typing import Any +from typing import TYPE_CHECKING, Any import dataclasses import textwrap -from .types import SingleEntity +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping + + from .types import SingleEntity class Role: @@ -85,7 +87,7 @@ def __init__(self, description: Mapping[str, Any], entity: SingleEntity) -> None key: value for key, value in description.items() if key in {"key", "plural", "label", "doc"} - } + }, ) self.entity = entity self.max = description.get("max") @@ -96,7 +98,7 @@ def __repr__(self) -> str: @dataclasses.dataclass(frozen=True) class _Description: - """A Role's description. + r"""A Role's description. Examples: >>> data = { diff --git a/openfisca_core/entities/tests/test_entity.py b/openfisca_core/entities/tests/test_entity.py index 488d271ff5..b3cb813ddc 100644 --- a/openfisca_core/entities/tests/test_entity.py +++ b/openfisca_core/entities/tests/test_entity.py @@ -3,7 +3,6 @@ def test_init_when_doc_indented() -> None: """De-indent the ``doc`` attribute if it is passed at initialisation.""" - key = "\tkey" doc = "\tdoc" entity = entities.Entity(key, "label", "plural", doc) diff --git a/openfisca_core/entities/tests/test_group_entity.py b/openfisca_core/entities/tests/test_group_entity.py index ed55648d71..092c9d3575 100644 --- a/openfisca_core/entities/tests/test_group_entity.py +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -43,7 +43,6 @@ def group_entity(role: Mapping[str, Any]) -> entities.GroupEntity: def test_init_when_doc_indented() -> None: """De-indent the ``doc`` attribute if it is passed at initialisation.""" - key = "\tkey" doc = "\tdoc" group_entity = entities.GroupEntity(key, "label", "plural", doc, ()) @@ -52,18 +51,20 @@ def test_init_when_doc_indented() -> None: def test_group_entity_with_roles( - group_entity: entities.GroupEntity, parent: str, uncle: str + group_entity: entities.GroupEntity, + parent: str, + uncle: str, ) -> None: """Assign a Role for each role-like passed as argument.""" - assert hasattr(group_entity, parent.upper()) assert not hasattr(group_entity, uncle.upper()) def test_group_entity_with_subroles( - group_entity: entities.GroupEntity, first_parent: str, second_parent: str + group_entity: entities.GroupEntity, + first_parent: str, + second_parent: str, ) -> None: """Assign a Role for each subrole-like passed as argument.""" - assert hasattr(group_entity, first_parent.upper()) assert not hasattr(group_entity, second_parent.upper()) diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py index 83692e8236..ffb1fdddb8 100644 --- a/openfisca_core/entities/tests/test_role.py +++ b/openfisca_core/entities/tests/test_role.py @@ -3,7 +3,6 @@ def test_init_when_doc_indented() -> None: """De-indent the ``doc`` attribute if it is passed at initialisation.""" - key = "\tkey" doc = "\tdoc" role = entities.Role({"key": key, "doc": doc}, object()) diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index 2f9acd0402..a6bf5e189f 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -1,11 +1,13 @@ from __future__ import annotations -from collections.abc import Iterable -from typing import NewType, Protocol +from typing import TYPE_CHECKING, NewType, Protocol from typing_extensions import Required, TypedDict from openfisca_core import types as t +if TYPE_CHECKING: + from collections.abc import Iterable + # Entities #: For example "person". @@ -26,12 +28,10 @@ class CoreEntity(t.CoreEntity, Protocol): plural: EntityPlural | None -class SingleEntity(t.SingleEntity, Protocol): - ... +class SingleEntity(t.SingleEntity, Protocol): ... -class GroupEntity(t.GroupEntity, Protocol): - ... +class GroupEntity(t.GroupEntity, Protocol): ... class Role(t.Role, Protocol): @@ -50,12 +50,10 @@ class RoleParams(TypedDict, total=False): # Tax-Benefit systems -class TaxBenefitSystem(t.TaxBenefitSystem, Protocol): - ... +class TaxBenefitSystem(t.TaxBenefitSystem, Protocol): ... # Variables -class Variable(t.Variable, Protocol): - ... +class Variable(t.Variable, Protocol): ... diff --git a/openfisca_core/errors/__init__.py b/openfisca_core/errors/__init__.py index 41d4760bee..2c4d438116 100644 --- a/openfisca_core/errors/__init__.py +++ b/openfisca_core/errors/__init__.py @@ -24,18 +24,22 @@ from .cycle_error import CycleError from .empty_argument_error import EmptyArgumentError from .nan_creation_error import NaNCreationError -from .parameter_not_found_error import ParameterNotFoundError -from .parameter_not_found_error import ParameterNotFoundError as ParameterNotFound +from .parameter_not_found_error import ( + ParameterNotFoundError, + ParameterNotFoundError as ParameterNotFound, +) from .parameter_parsing_error import ParameterParsingError from .period_mismatch_error import PeriodMismatchError from .situation_parsing_error import SituationParsingError from .spiral_error import SpiralError -from .variable_name_config_error import VariableNameConflictError from .variable_name_config_error import ( + VariableNameConflictError, VariableNameConflictError as VariableNameConflict, ) -from .variable_not_found_error import VariableNotFoundError -from .variable_not_found_error import VariableNotFoundError as VariableNotFound +from .variable_not_found_error import ( + VariableNotFoundError, + VariableNotFoundError as VariableNotFound, +) __all__ = [ "CycleError", diff --git a/openfisca_core/errors/cycle_error.py b/openfisca_core/errors/cycle_error.py index b4d44b5993..b81cc7b3f9 100644 --- a/openfisca_core/errors/cycle_error.py +++ b/openfisca_core/errors/cycle_error.py @@ -1,4 +1,2 @@ class CycleError(Exception): """Simulation error.""" - - pass diff --git a/openfisca_core/errors/empty_argument_error.py b/openfisca_core/errors/empty_argument_error.py index 0d0205b432..960d8d28c2 100644 --- a/openfisca_core/errors/empty_argument_error.py +++ b/openfisca_core/errors/empty_argument_error.py @@ -16,7 +16,7 @@ def __init__( class_name: str, method_name: str, arg_name: str, - arg_value: typing.Union[typing.List, numpy.ndarray], + arg_value: typing.Union[list, numpy.ndarray], ) -> None: message = [ f"'{class_name}.{method_name}' can't be run with an empty '{arg_name}':\n", diff --git a/openfisca_core/errors/nan_creation_error.py b/openfisca_core/errors/nan_creation_error.py index dfd1b7af7e..373e391517 100644 --- a/openfisca_core/errors/nan_creation_error.py +++ b/openfisca_core/errors/nan_creation_error.py @@ -1,4 +1,2 @@ class NaNCreationError(Exception): """Simulation error.""" - - pass diff --git a/openfisca_core/errors/parameter_not_found_error.py b/openfisca_core/errors/parameter_not_found_error.py index 1a8528f45c..bad33c89f4 100644 --- a/openfisca_core/errors/parameter_not_found_error.py +++ b/openfisca_core/errors/parameter_not_found_error.py @@ -1,21 +1,16 @@ class ParameterNotFoundError(AttributeError): - """ - Exception raised when a parameter is not found in the parameters. - """ + """Exception raised when a parameter is not found in the parameters.""" - def __init__(self, name, instant_str, variable_name=None): - """ - :param name: Name of the parameter + def __init__(self, name, instant_str, variable_name=None) -> None: + """:param name: Name of the parameter :param instant_str: Instant where the parameter does not exist, in the format `YYYY-MM-DD`. :param variable_name: If the parameter was queried during the computation of a variable, name of that variable. """ self.name = name self.instant_str = instant_str self.variable_name = variable_name - message = "The parameter '{}'".format(name) + message = f"The parameter '{name}'" if variable_name is not None: - message += " requested by variable '{}'".format(variable_name) - message += (" was not found in the {} tax and benefit system.").format( - instant_str - ) - super(ParameterNotFoundError, self).__init__(message) + message += f" requested by variable '{variable_name}'" + message += f" was not found in the {instant_str} tax and benefit system." + super().__init__(message) diff --git a/openfisca_core/errors/parameter_parsing_error.py b/openfisca_core/errors/parameter_parsing_error.py index 48b44e3341..7628e42d86 100644 --- a/openfisca_core/errors/parameter_parsing_error.py +++ b/openfisca_core/errors/parameter_parsing_error.py @@ -2,20 +2,17 @@ class ParameterParsingError(Exception): - """ - Exception raised when a parameter cannot be parsed. - """ + """Exception raised when a parameter cannot be parsed.""" - def __init__(self, message, file=None, traceback=None): - """ - :param message: Error message + def __init__(self, message, file=None, traceback=None) -> None: + """:param message: Error message :param file: Parameter file which caused the error (optional) :param traceback: Traceback (optional) """ if file is not None: message = os.linesep.join( - ["Error parsing parameter file '{}':".format(file), message] + [f"Error parsing parameter file '{file}':", message], ) if traceback is not None: message = os.linesep.join([traceback, message]) - super(ParameterParsingError, self).__init__(message) + super().__init__(message) diff --git a/openfisca_core/errors/period_mismatch_error.py b/openfisca_core/errors/period_mismatch_error.py index 2937d11968..fcece9474d 100644 --- a/openfisca_core/errors/period_mismatch_error.py +++ b/openfisca_core/errors/period_mismatch_error.py @@ -1,9 +1,7 @@ class PeriodMismatchError(ValueError): - """ - Exception raised when one tries to set a variable value for a period that doesn't match its definition period - """ + """Exception raised when one tries to set a variable value for a period that doesn't match its definition period.""" - def __init__(self, variable_name: str, period, definition_period, message): + def __init__(self, variable_name: str, period, definition_period, message) -> None: self.variable_name = variable_name self.period = period self.definition_period = definition_period diff --git a/openfisca_core/errors/situation_parsing_error.py b/openfisca_core/errors/situation_parsing_error.py index ff3839d5f7..6563d1da2a 100644 --- a/openfisca_core/errors/situation_parsing_error.py +++ b/openfisca_core/errors/situation_parsing_error.py @@ -1,19 +1,23 @@ from __future__ import annotations -from collections.abc import Iterable +from typing import TYPE_CHECKING import os import dpath.util +if TYPE_CHECKING: + from collections.abc import Iterable + class SituationParsingError(Exception): - """ - Exception raised when the situation provided as an input for a simulation cannot be parsed - """ + """Exception raised when the situation provided as an input for a simulation cannot be parsed.""" def __init__( - self, path: Iterable[str], message: str, code: int | None = None + self, + path: Iterable[str], + message: str, + code: int | None = None, ) -> None: self.error = {} dpath_path = "/".join([str(item) for item in path]) diff --git a/openfisca_core/errors/spiral_error.py b/openfisca_core/errors/spiral_error.py index 0495439b68..ffa7fe2850 100644 --- a/openfisca_core/errors/spiral_error.py +++ b/openfisca_core/errors/spiral_error.py @@ -1,4 +1,2 @@ class SpiralError(Exception): """Simulation error.""" - - pass diff --git a/openfisca_core/errors/variable_name_config_error.py b/openfisca_core/errors/variable_name_config_error.py index 7a87d7f5c8..fec1c45864 100644 --- a/openfisca_core/errors/variable_name_config_error.py +++ b/openfisca_core/errors/variable_name_config_error.py @@ -1,6 +1,2 @@ class VariableNameConflictError(Exception): - """ - Exception raised when two variables with the same name are added to a tax and benefit system. - """ - - pass + """Exception raised when two variables with the same name are added to a tax and benefit system.""" diff --git a/openfisca_core/errors/variable_not_found_error.py b/openfisca_core/errors/variable_not_found_error.py index ab71239c7d..46ece4b13c 100644 --- a/openfisca_core/errors/variable_not_found_error.py +++ b/openfisca_core/errors/variable_not_found_error.py @@ -2,36 +2,27 @@ class VariableNotFoundError(Exception): - """ - Exception raised when a variable has been queried but is not defined in the TaxBenefitSystem. - """ + """Exception raised when a variable has been queried but is not defined in the TaxBenefitSystem.""" - def __init__(self, variable_name: str, tax_benefit_system): - """ - :param variable_name: Name of the variable that was queried. + def __init__(self, variable_name: str, tax_benefit_system) -> None: + """:param variable_name: Name of the variable that was queried. :param tax_benefit_system: Tax benefits system that does not contain `variable_name` """ country_package_metadata = tax_benefit_system.get_package_metadata() country_package_name = country_package_metadata["name"] country_package_version = country_package_metadata["version"] if country_package_version: - country_package_id = "{}@{}".format( - country_package_name, country_package_version - ) + country_package_id = f"{country_package_name}@{country_package_version}" else: country_package_id = country_package_name message = os.linesep.join( [ - "You tried to calculate or to set a value for variable '{0}', but it was not found in the loaded tax and benefit system ({1}).".format( - variable_name, country_package_id - ), - "Are you sure you spelled '{0}' correctly?".format(variable_name), + f"You tried to calculate or to set a value for variable '{variable_name}', but it was not found in the loaded tax and benefit system ({country_package_id}).", + f"Are you sure you spelled '{variable_name}' correctly?", "If this code used to work and suddenly does not, this is most probably linked to an update of the tax and benefit system.", "Look at its changelog to learn about renames and removals and update your code. If it is an official package,", - "it is probably available on .".format( - country_package_name - ), - ] + f"it is probably available on .", + ], ) self.message = message self.variable_name = variable_name diff --git a/openfisca_core/experimental/memory_config.py b/openfisca_core/experimental/memory_config.py index b5a0af5317..fec38e3a54 100644 --- a/openfisca_core/experimental/memory_config.py +++ b/openfisca_core/experimental/memory_config.py @@ -5,8 +5,11 @@ class MemoryConfig: def __init__( - self, max_memory_occupation, priority_variables=None, variables_to_drop=None - ): + self, + max_memory_occupation, + priority_variables=None, + variables_to_drop=None, + ) -> None: message = [ "Memory configuration is a feature that is still currently under experimentation.", "You are very welcome to use it and send us precious feedback,", @@ -16,7 +19,8 @@ def __init__( self.max_memory_occupation = float(max_memory_occupation) if self.max_memory_occupation > 1: - raise ValueError("max_memory_occupation must be <= 1") + msg = "max_memory_occupation must be <= 1" + raise ValueError(msg) self.max_memory_occupation_pc = self.max_memory_occupation * 100 self.priority_variables = ( set(priority_variables) if priority_variables else set() diff --git a/openfisca_core/holders/helpers.py b/openfisca_core/holders/helpers.py index 0e88964fc7..fcc6563c79 100644 --- a/openfisca_core/holders/helpers.py +++ b/openfisca_core/holders/helpers.py @@ -7,9 +7,8 @@ log = logging.getLogger(__name__) -def set_input_dispatch_by_period(holder, period, array): - """ - This function can be declared as a ``set_input`` attribute of a variable. +def set_input_dispatch_by_period(holder, period, array) -> None: + """This function can be declared as a ``set_input`` attribute of a variable. In this case, the variable will accept inputs on larger periods that its definition period, and the value for the larger period will be applied to all its subperiods. @@ -23,8 +22,9 @@ def set_input_dispatch_by_period(holder, period, array): if holder.variable.definition_period not in ( periods.DateUnit.isoformat + periods.DateUnit.isocalendar ): + msg = "set_input_dispatch_by_period can't be used for eternal variables." raise ValueError( - "set_input_dispatch_by_period can't be used for eternal variables." + msg, ) cached_period_unit = holder.variable.definition_period @@ -43,9 +43,8 @@ def set_input_dispatch_by_period(holder, period, array): sub_period = sub_period.offset(1) -def set_input_divide_by_period(holder, period, array): - """ - This function can be declared as a ``set_input`` attribute of a variable. +def set_input_divide_by_period(holder, period, array) -> None: + """This function can be declared as a ``set_input`` attribute of a variable. In this case, the variable will accept inputs on larger periods that its definition period, and the value for the larger period will be divided between its subperiods. @@ -59,8 +58,9 @@ def set_input_divide_by_period(holder, period, array): if holder.variable.definition_period not in ( periods.DateUnit.isoformat + periods.DateUnit.isocalendar ): + msg = "set_input_divide_by_period can't be used for eternal variables." raise ValueError( - "set_input_divide_by_period can't be used for eternal variables." + msg, ) cached_period_unit = holder.variable.definition_period @@ -87,8 +87,7 @@ def set_input_divide_by_period(holder, period, array): holder._set(sub_period, divided_array) sub_period = sub_period.offset(1) elif not (remaining_array == 0).all(): + msg = f"Inconsistent input: variable {holder.variable.name} has already been set for all months contained in period {period}, and value {array} provided for {period} doesn't match the total ({array - remaining_array}). This error may also be thrown if you try to call set_input twice for the same variable and period." raise ValueError( - "Inconsistent input: variable {0} has already been set for all months contained in period {1}, and value {2} provided for {1} doesn't match the total ({3}). This error may also be thrown if you try to call set_input twice for the same variable and period.".format( - holder.variable.name, period, array, array - remaining_array - ) + msg, ) diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index 230c916d06..8e30b4bb2f 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any import os import warnings @@ -8,21 +8,26 @@ import numpy import psutil -from openfisca_core import commons -from openfisca_core import data_storage as storage -from openfisca_core import errors -from openfisca_core import indexed_enums as enums -from openfisca_core import periods, tools, types +from openfisca_core import ( + commons, + data_storage as storage, + errors, + indexed_enums as enums, + periods, + tools, + types, +) from .memory_usage import MemoryUsage +if TYPE_CHECKING: + from collections.abc import Sequence + class Holder: - """ - A holder keeps tracks of a variable values after they have been calculated, or set as an input. - """ + """A holder keeps tracks of a variable values after they have been calculated, or set as an input.""" - def __init__(self, variable, population): + def __init__(self, variable, population) -> None: self.population = population self.variable = variable self.simulation = population.simulation @@ -44,9 +49,7 @@ def __init__(self, variable, population): self._do_not_store = True def clone(self, population): - """ - Copy the holder just enough to be able to run a new simulation without modifying the original simulation. - """ + """Copy the holder just enough to be able to run a new simulation without modifying the original simulation.""" new = commons.empty_clone(self) new_dict = new.__dict__ @@ -66,23 +69,22 @@ def create_disk_storage(self, directory=None, preserve=False): if not os.path.isdir(storage_dir): os.mkdir(storage_dir) return storage.OnDiskStorage( - storage_dir, self._eternal, preserve_storage_dir=preserve + storage_dir, + self._eternal, + preserve_storage_dir=preserve, ) - def delete_arrays(self, period=None): - """ - If ``period`` is ``None``, remove all known values of the variable. + def delete_arrays(self, period=None) -> None: + """If ``period`` is ``None``, remove all known values of the variable. If ``period`` is not ``None``, only remove all values for any period included in period (e.g. if period is "2017", values for "2017-01", "2017-07", etc. would be removed) """ - self._memory_storage.delete(period) if self._disk_storage: self._disk_storage.delete(period) def get_array(self, period): - """ - Get the value of the variable for the given period. + """Get the value of the variable for the given period. If the value is not known, return ``None``. """ @@ -93,6 +95,7 @@ def get_array(self, period): return value if self._disk_storage: return self._disk_storage.get(period) + return None def get_memory_usage(self) -> MemoryUsage: """Get data about the virtual memory usage of the Holder. @@ -109,7 +112,7 @@ def get_memory_usage(self) -> MemoryUsage: ... simulations, ... taxbenefitsystems, ... variables, - ... ) + ... ) >>> entity = entities.Entity("", "", "", "") @@ -127,7 +130,7 @@ def get_memory_usage(self) -> MemoryUsage: >>> simulation = simulations.Simulation(tbs, entities) >>> holder.simulation = simulation - >>> pprint(holder.get_memory_usage(), indent = 3) + >>> pprint(holder.get_memory_usage(), indent=3) { 'cell_size': nan, 'dtype': , 'nb_arrays': 0, @@ -135,7 +138,6 @@ def get_memory_usage(self) -> MemoryUsage: 'total_nb_bytes': 0... """ - usage = MemoryUsage( nb_cells_by_array=self.population.count, dtype=self.variable.dtype, @@ -146,30 +148,29 @@ def get_memory_usage(self) -> MemoryUsage: if self.simulation.trace: nb_requests = self.simulation.tracer.get_nb_requests(self.variable.name) usage.update( - dict( - nb_requests=nb_requests, - nb_requests_by_array=nb_requests / float(usage["nb_arrays"]) - if usage["nb_arrays"] > 0 - else numpy.nan, - ) + { + "nb_requests": nb_requests, + "nb_requests_by_array": ( + nb_requests / float(usage["nb_arrays"]) + if usage["nb_arrays"] > 0 + else numpy.nan + ), + }, ) return usage def get_known_periods(self): - """ - Get the list of periods the variable value is known for. - """ - + """Get the list of periods the variable value is known for.""" return list(self._memory_storage.get_known_periods()) + list( - (self._disk_storage.get_known_periods() if self._disk_storage else []) + self._disk_storage.get_known_periods() if self._disk_storage else [], ) def set_input( self, period: types.Period, - array: Union[numpy.ndarray, Sequence[Any]], - ) -> Optional[numpy.ndarray]: + array: numpy.ndarray | Sequence[Any], + ) -> numpy.ndarray | None: """Set a Variable's array of values of a given Period. Args: @@ -187,6 +188,7 @@ def set_input( Examples: >>> from openfisca_core import entities, populations, variables + >>> entity = entities.Entity("", "", "", "") >>> class MyVariable(variables.Variable): @@ -212,7 +214,6 @@ def set_input( https://openfisca.org/doc/coding-the-legislation/35_periods.html#set-input-automatically-process-variable-inputs-defined-for-periods-not-matching-the-definition-period """ - period = periods.period(period) if period.unit == periods.DateUnit.ETERNITY and not self._eternal: @@ -220,7 +221,7 @@ def set_input( [ "Unable to set a value for variable {1} for {0}.", "{1} is only defined for {2}s. Please adapt your input.", - ] + ], ).format( periods.DateUnit.ETERNITY.upper(), self.variable.name, @@ -233,9 +234,7 @@ def set_input( error_message, ) if self.variable.is_neutralized: - warning_message = "You cannot set a value for the variable {}, as it has been neutralized. The value you provided ({}) will be ignored.".format( - self.variable.name, array - ) + warning_message = f"You cannot set a value for the variable {self.variable.name}, as it has been neutralized. The value you provided ({array}) will be ignored." return warnings.warn(warning_message, Warning, stacklevel=2) if self.variable.value_type in (float, int) and isinstance(array, str): array = tools.eval_expression(array) @@ -250,14 +249,9 @@ def _to_array(self, value): # 0-dim arrays are casted to scalar when they interact with float. We don't want that. value = value.reshape(1) if len(value) != self.population.count: + msg = f'Unable to set value "{value}" for variable "{self.variable.name}", as its length is {len(value)} while there are {self.population.count} {self.population.entity.plural} in the simulation.' raise ValueError( - 'Unable to set value "{}" for variable "{}", as its length is {} while there are {} {} in the simulation.'.format( - value, - self.variable.name, - len(value), - self.population.count, - self.population.entity.plural, - ) + msg, ) if self.variable.value_type == enums.Enum: value = self.variable.possible_values.encode(value) @@ -265,20 +259,22 @@ def _to_array(self, value): try: value = value.astype(self.variable.dtype) except ValueError: + msg = f'Unable to set value "{value}" for variable "{self.variable.name}", as the variable dtype "{self.variable.dtype}" does not match the value dtype "{value.dtype}".' raise ValueError( - 'Unable to set value "{}" for variable "{}", as the variable dtype "{}" does not match the value dtype "{}".'.format( - value, self.variable.name, self.variable.dtype, value.dtype - ) + msg, ) return value - def _set(self, period, value): + def _set(self, period, value) -> None: value = self._to_array(value) if not self._eternal: if period is None: - raise ValueError( + msg = ( f"A period must be specified to set values, except for variables with " - f"{periods.DateUnit.ETERNITY.upper()} as as period_definition.", + f"{periods.DateUnit.ETERNITY.upper()} as as period_definition." + ) + raise ValueError( + msg, ) if self.variable.definition_period != period.unit or period.size > 1: name = self.variable.name @@ -292,7 +288,7 @@ def _set(self, period, value): f'Unable to set a value for variable "{name}" for {period_size_adj}-long period "{period}".', f'"{name}" can only be set for one {self.variable.definition_period} at a time. Please adapt your input.', f'If you are the maintainer of "{name}", you can consider adding it a set_input attribute to enable automatic period casting.', - ] + ], ) raise errors.PeriodMismatchError( @@ -314,7 +310,7 @@ def _set(self, period, value): else: self._memory_storage.put(value, period) - def put_in_cache(self, value, period): + def put_in_cache(self, value, period) -> None: if self._do_not_store: return @@ -328,8 +324,5 @@ def put_in_cache(self, value, period): self._set(period, value) def default_array(self): - """ - Return a new array of the appropriate length for the entity, filled with the variable default values. - """ - + """Return a new array of the appropriate length for the entity, filled with the variable default values.""" return self.variable.default_array(self.population.count) diff --git a/openfisca_core/holders/tests/test_helpers.py b/openfisca_core/holders/tests/test_helpers.py index d76040d676..948f25288f 100644 --- a/openfisca_core/holders/tests/test_helpers.py +++ b/openfisca_core/holders/tests/test_helpers.py @@ -35,38 +35,33 @@ def population(people): @pytest.mark.parametrize( - "dispatch_unit, definition_unit, values, expected", + ("dispatch_unit", "definition_unit", "values", "expected"), [ - [DateUnit.YEAR, DateUnit.YEAR, [1.0], [3.0]], - [DateUnit.YEAR, DateUnit.MONTH, [1.0], [36.0]], - [DateUnit.YEAR, DateUnit.DAY, [1.0], [1096.0]], - [DateUnit.YEAR, DateUnit.WEEK, [1.0], [157.0]], - [DateUnit.YEAR, DateUnit.WEEKDAY, [1.0], [1096.0]], - [DateUnit.MONTH, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.MONTH, DateUnit.MONTH, [1.0], [3.0]], - [DateUnit.MONTH, DateUnit.DAY, [1.0], [90.0]], - [DateUnit.MONTH, DateUnit.WEEK, [1.0], [13.0]], - [DateUnit.MONTH, DateUnit.WEEKDAY, [1.0], [90.0]], - [DateUnit.DAY, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.DAY, [1.0], [3.0]], - [DateUnit.DAY, DateUnit.WEEK, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.WEEKDAY, [1.0], [3.0]], - [DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.DAY, [1.0], [21.0]], - [DateUnit.WEEK, DateUnit.WEEK, [1.0], [3.0]], - [DateUnit.WEEK, DateUnit.WEEKDAY, [1.0], [21.0]], - [DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.DAY, [1.0], [21.0]], - [DateUnit.WEEK, DateUnit.WEEK, [1.0], [3.0]], - [DateUnit.WEEK, DateUnit.WEEKDAY, [1.0], [21.0]], - [DateUnit.WEEKDAY, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.DAY, [1.0], [3.0]], - [DateUnit.WEEKDAY, DateUnit.WEEK, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.WEEKDAY, [1.0], [3.0]], + (DateUnit.YEAR, DateUnit.YEAR, [1.0], [3.0]), + (DateUnit.YEAR, DateUnit.MONTH, [1.0], [36.0]), + (DateUnit.YEAR, DateUnit.DAY, [1.0], [1096.0]), + (DateUnit.YEAR, DateUnit.WEEK, [1.0], [157.0]), + (DateUnit.YEAR, DateUnit.WEEKDAY, [1.0], [1096.0]), + (DateUnit.MONTH, DateUnit.YEAR, [1.0], [1.0]), + (DateUnit.MONTH, DateUnit.MONTH, [1.0], [3.0]), + (DateUnit.MONTH, DateUnit.DAY, [1.0], [90.0]), + (DateUnit.MONTH, DateUnit.WEEK, [1.0], [13.0]), + (DateUnit.MONTH, DateUnit.WEEKDAY, [1.0], [90.0]), + (DateUnit.DAY, DateUnit.YEAR, [1.0], [1.0]), + (DateUnit.DAY, DateUnit.MONTH, [1.0], [1.0]), + (DateUnit.DAY, DateUnit.DAY, [1.0], [3.0]), + (DateUnit.DAY, DateUnit.WEEK, [1.0], [1.0]), + (DateUnit.DAY, DateUnit.WEEKDAY, [1.0], [3.0]), + (DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]), + (DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]), + (DateUnit.WEEK, DateUnit.DAY, [1.0], [21.0]), + (DateUnit.WEEK, DateUnit.WEEK, [1.0], [3.0]), + (DateUnit.WEEK, DateUnit.WEEKDAY, [1.0], [21.0]), + (DateUnit.WEEKDAY, DateUnit.YEAR, [1.0], [1.0]), + (DateUnit.WEEKDAY, DateUnit.MONTH, [1.0], [1.0]), + (DateUnit.WEEKDAY, DateUnit.DAY, [1.0], [3.0]), + (DateUnit.WEEKDAY, DateUnit.WEEK, [1.0], [1.0]), + (DateUnit.WEEKDAY, DateUnit.WEEKDAY, [1.0], [3.0]), ], ) def test_set_input_dispatch_by_period( @@ -76,7 +71,7 @@ def test_set_input_dispatch_by_period( definition_unit, values, expected, -): +) -> None: Income.definition_period = definition_unit income = Income() holder = Holder(income, population) @@ -90,33 +85,33 @@ def test_set_input_dispatch_by_period( @pytest.mark.parametrize( - "divide_unit, definition_unit, values, expected", + ("divide_unit", "definition_unit", "values", "expected"), [ - [DateUnit.YEAR, DateUnit.YEAR, [3.0], [1.0]], - [DateUnit.YEAR, DateUnit.MONTH, [36.0], [1.0]], - [DateUnit.YEAR, DateUnit.DAY, [1095.0], [1.0]], - [DateUnit.YEAR, DateUnit.WEEK, [157.0], [1.0]], - [DateUnit.YEAR, DateUnit.WEEKDAY, [1095.0], [1.0]], - [DateUnit.MONTH, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.MONTH, DateUnit.MONTH, [3.0], [1.0]], - [DateUnit.MONTH, DateUnit.DAY, [90.0], [1.0]], - [DateUnit.MONTH, DateUnit.WEEK, [13.0], [1.0]], - [DateUnit.MONTH, DateUnit.WEEKDAY, [90.0], [1.0]], - [DateUnit.DAY, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.DAY, [3.0], [1.0]], - [DateUnit.DAY, DateUnit.WEEK, [1.0], [1.0]], - [DateUnit.DAY, DateUnit.WEEKDAY, [3.0], [1.0]], - [DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEK, DateUnit.DAY, [21.0], [1.0]], - [DateUnit.WEEK, DateUnit.WEEK, [3.0], [1.0]], - [DateUnit.WEEK, DateUnit.WEEKDAY, [21.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.YEAR, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.MONTH, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.DAY, [3.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.WEEK, [1.0], [1.0]], - [DateUnit.WEEKDAY, DateUnit.WEEKDAY, [3.0], [1.0]], + (DateUnit.YEAR, DateUnit.YEAR, [3.0], [1.0]), + (DateUnit.YEAR, DateUnit.MONTH, [36.0], [1.0]), + (DateUnit.YEAR, DateUnit.DAY, [1095.0], [1.0]), + (DateUnit.YEAR, DateUnit.WEEK, [157.0], [1.0]), + (DateUnit.YEAR, DateUnit.WEEKDAY, [1095.0], [1.0]), + (DateUnit.MONTH, DateUnit.YEAR, [1.0], [1.0]), + (DateUnit.MONTH, DateUnit.MONTH, [3.0], [1.0]), + (DateUnit.MONTH, DateUnit.DAY, [90.0], [1.0]), + (DateUnit.MONTH, DateUnit.WEEK, [13.0], [1.0]), + (DateUnit.MONTH, DateUnit.WEEKDAY, [90.0], [1.0]), + (DateUnit.DAY, DateUnit.YEAR, [1.0], [1.0]), + (DateUnit.DAY, DateUnit.MONTH, [1.0], [1.0]), + (DateUnit.DAY, DateUnit.DAY, [3.0], [1.0]), + (DateUnit.DAY, DateUnit.WEEK, [1.0], [1.0]), + (DateUnit.DAY, DateUnit.WEEKDAY, [3.0], [1.0]), + (DateUnit.WEEK, DateUnit.YEAR, [1.0], [1.0]), + (DateUnit.WEEK, DateUnit.MONTH, [1.0], [1.0]), + (DateUnit.WEEK, DateUnit.DAY, [21.0], [1.0]), + (DateUnit.WEEK, DateUnit.WEEK, [3.0], [1.0]), + (DateUnit.WEEK, DateUnit.WEEKDAY, [21.0], [1.0]), + (DateUnit.WEEKDAY, DateUnit.YEAR, [1.0], [1.0]), + (DateUnit.WEEKDAY, DateUnit.MONTH, [1.0], [1.0]), + (DateUnit.WEEKDAY, DateUnit.DAY, [3.0], [1.0]), + (DateUnit.WEEKDAY, DateUnit.WEEK, [1.0], [1.0]), + (DateUnit.WEEKDAY, DateUnit.WEEKDAY, [3.0], [1.0]), ], ) def test_set_input_divide_by_period( @@ -126,7 +121,7 @@ def test_set_input_divide_by_period( definition_unit, values, expected, -): +) -> None: Income.definition_period = definition_unit income = Income() holder = Holder(income, population) diff --git a/openfisca_core/indexed_enums/enum.py b/openfisca_core/indexed_enums/enum.py index 7957ced3a2..25b02cee74 100644 --- a/openfisca_core/indexed_enums/enum.py +++ b/openfisca_core/indexed_enums/enum.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Union - import enum import numpy @@ -11,8 +9,7 @@ class Enum(enum.Enum): - """ - Enum based on `enum34 `_, whose items + """Enum based on `enum34 `_, whose items have an index. """ @@ -33,15 +30,9 @@ def __init__(self, name: str) -> None: @classmethod def encode( cls, - array: Union[ - EnumArray, - numpy.int_, - numpy.float_, - numpy.object_, - ], + array: EnumArray | numpy.int_ | numpy.float64 | numpy.object_, ) -> EnumArray: - """ - Encode a string numpy array, an enum item numpy array, or an int numpy + """Encode a string numpy array, an enum item numpy array, or an int numpy array into an :any:`EnumArray`. See :any:`EnumArray.decode` for decoding. @@ -53,7 +44,7 @@ def encode( For instance: - >>> string_identifier_array = asarray(['free_lodger', 'owner']) + >>> string_identifier_array = asarray(["free_lodger", "owner"]) >>> encoded_array = HousingOccupancyStatus.encode(string_identifier_array) >>> encoded_array[0] 2 # Encoded value diff --git a/openfisca_core/indexed_enums/enum_array.py b/openfisca_core/indexed_enums/enum_array.py index 2742719ada..86b55f9f48 100644 --- a/openfisca_core/indexed_enums/enum_array.py +++ b/openfisca_core/indexed_enums/enum_array.py @@ -1,7 +1,7 @@ from __future__ import annotations import typing -from typing import Any, NoReturn, Optional, Type +from typing import Any, NoReturn import numpy @@ -10,8 +10,7 @@ class EnumArray(numpy.ndarray): - """ - NumPy array subclass representing an array of enum items. + """NumPy array subclass representing an array of enum items. EnumArrays are encoded as ``int`` arrays to improve performance """ @@ -22,20 +21,20 @@ class EnumArray(numpy.ndarray): def __new__( cls, input_array: numpy.int_, - possible_values: Optional[Type[Enum]] = None, + possible_values: type[Enum] | None = None, ) -> EnumArray: obj = numpy.asarray(input_array).view(cls) obj.possible_values = possible_values return obj # See previous comment - def __array_finalize__(self, obj: Optional[numpy.int_]) -> None: + def __array_finalize__(self, obj: numpy.int_ | None) -> None: if obj is None: return self.possible_values = getattr(obj, "possible_values", None) - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: # When comparing to an item of self.possible_values, use the item index # to speed up the comparison. if other.__class__.__name__ is self.possible_values.__name__: @@ -45,13 +44,16 @@ def __eq__(self, other: Any) -> bool: return self.view(numpy.ndarray) == other - def __ne__(self, other: Any) -> bool: + def __ne__(self, other: object) -> bool: return numpy.logical_not(self == other) def _forbidden_operation(self, other: Any) -> NoReturn: - raise TypeError( + msg = ( "Forbidden operation. The only operations allowed on EnumArrays " - "are '==' and '!='.", + "are '==' and '!='." + ) + raise TypeError( + msg, ) __add__ = _forbidden_operation @@ -64,12 +66,11 @@ def _forbidden_operation(self, other: Any) -> NoReturn: __or__ = _forbidden_operation def decode(self) -> numpy.object_: - """ - Return the array of enum items corresponding to self. + """Return the array of enum items corresponding to self. For instance: - >>> enum_array = household('housing_occupancy_status', period) + >>> enum_array = household("housing_occupancy_status", period) >>> enum_array[0] >>> 2 # Encoded value >>> enum_array.decode()[0] @@ -83,12 +84,11 @@ def decode(self) -> numpy.object_: ) def decode_to_str(self) -> numpy.str_: - """ - Return the array of string identifiers corresponding to self. + """Return the array of string identifiers corresponding to self. For instance: - >>> enum_array = household('housing_occupancy_status', period) + >>> enum_array = household("housing_occupancy_status", period) >>> enum_array[0] >>> 2 # Encoded value >>> enum_array.decode_to_str()[0] @@ -100,7 +100,7 @@ def decode_to_str(self) -> numpy.str_: ) def __repr__(self) -> str: - return f"{self.__class__.__name__}({str(self.decode())})" + return f"{self.__class__.__name__}({self.decode()!s})" def __str__(self) -> str: return str(self.decode_to_str()) diff --git a/openfisca_core/model_api.py b/openfisca_core/model_api.py index 553ee75b34..e36e0d5f76 100644 --- a/openfisca_core/model_api.py +++ b/openfisca_core/model_api.py @@ -1,10 +1,13 @@ from datetime import date -from numpy import logical_not as not_ -from numpy import maximum as max_ -from numpy import minimum as min_ -from numpy import round as round_ -from numpy import select, where +from numpy import ( + logical_not as not_, + maximum as max_, + minimum as min_, + round as round_, + select, + where, +) from openfisca_core.commons import apply_thresholds, concat, switch from openfisca_core.holders import ( diff --git a/openfisca_core/parameters/__init__.py b/openfisca_core/parameters/__init__.py index f64f577fd4..5d742d4611 100644 --- a/openfisca_core/parameters/__init__.py +++ b/openfisca_core/parameters/__init__.py @@ -36,10 +36,11 @@ from .parameter_at_instant import ParameterAtInstant from .parameter_node import ParameterNode from .parameter_node_at_instant import ParameterNodeAtInstant -from .parameter_scale import ParameterScale -from .parameter_scale import ParameterScale as Scale -from .parameter_scale_bracket import ParameterScaleBracket -from .parameter_scale_bracket import ParameterScaleBracket as Bracket +from .parameter_scale import ParameterScale, ParameterScale as Scale +from .parameter_scale_bracket import ( + ParameterScaleBracket, + ParameterScaleBracket as Bracket, +) from .values_history import ValuesHistory from .vectorial_asof_date_parameter_node_at_instant import ( VectorialAsofDateParameterNodeAtInstant, diff --git a/openfisca_core/parameters/at_instant_like.py b/openfisca_core/parameters/at_instant_like.py index 1a1db34beb..19c28e98c2 100644 --- a/openfisca_core/parameters/at_instant_like.py +++ b/openfisca_core/parameters/at_instant_like.py @@ -4,9 +4,7 @@ class AtInstantLike(abc.ABC): - """ - Base class for various types of parameters implementing the at instant protocol. - """ + """Base class for various types of parameters implementing the at instant protocol.""" def __call__(self, instant): return self.get_at_instant(instant) @@ -16,5 +14,4 @@ def get_at_instant(self, instant): return self._get_at_instant(instant) @abc.abstractmethod - def _get_at_instant(self, instant): - ... + def _get_at_instant(self, instant): ... diff --git a/openfisca_core/parameters/config.py b/openfisca_core/parameters/config.py index 1900d0f550..b97462a79d 100644 --- a/openfisca_core/parameters/config.py +++ b/openfisca_core/parameters/config.py @@ -1,5 +1,3 @@ -import typing - import os import warnings @@ -23,7 +21,7 @@ # 'unit' and 'reference' are only listed here for backward compatibility. # It is now recommended to include them in metadata, until a common consensus emerges. -ALLOWED_PARAM_TYPES = (float, int, bool, type(None), typing.List) +ALLOWED_PARAM_TYPES = (float, int, bool, type(None), list) COMMON_KEYS = {"description", "metadata", "unit", "reference", "documentation"} FILE_EXTENSIONS = {".yaml", ".yml"} @@ -39,9 +37,12 @@ def dict_no_duplicate_constructor(loader, node, deep=False): keys = [key.value for key, value in node.value] if len(keys) != len(set(keys)): - duplicate = next((key for key in keys if keys.count(key) > 1)) + duplicate = next(key for key in keys if keys.count(key) > 1) + msg = "" raise yaml.parser.ParserError( - "", node.start_mark, f"Found duplicate key '{duplicate}'" + msg, + node.start_mark, + f"Found duplicate key '{duplicate}'", ) return loader.construct_mapping(node, deep) diff --git a/openfisca_core/parameters/helpers.py b/openfisca_core/parameters/helpers.py index 30af4adcbc..09925bbcdb 100644 --- a/openfisca_core/parameters/helpers.py +++ b/openfisca_core/parameters/helpers.py @@ -10,21 +10,21 @@ def contains_nan(vector): if numpy.issubdtype(vector.dtype, numpy.record) or numpy.issubdtype( - vector.dtype, numpy.void + vector.dtype, + numpy.void, ): - return any([contains_nan(vector[name]) for name in vector.dtype.names]) - else: - return numpy.isnan(vector).any() + return any(contains_nan(vector[name]) for name in vector.dtype.names) + return numpy.isnan(vector).any() def load_parameter_file(file_path, name=""): - """ - Load parameters from a YAML file (or a directory containing YAML files). + """Load parameters from a YAML file (or a directory containing YAML files). :returns: An instance of :class:`.ParameterNode` or :class:`.ParameterScale` or :class:`.Parameter`. """ if not os.path.exists(file_path): - raise ValueError("{} does not exist".format(file_path)) + msg = f"{file_path} does not exist" + raise ValueError(msg) if os.path.isdir(file_path): return parameters.ParameterNode(name, directory_path=file_path) data = _load_yaml_file(file_path) @@ -35,26 +35,29 @@ def _compose_name(path, child_name=None, item_name=None): if not path: return child_name if child_name is not None: - return "{}.{}".format(path, child_name) + return f"{path}.{child_name}" if item_name is not None: - return "{}[{}]".format(path, item_name) + return f"{path}[{item_name}]" + return None def _load_yaml_file(file_path): - with open(file_path, "r") as f: + with open(file_path) as f: try: return config.yaml.load(f, Loader=config.Loader) except (config.yaml.scanner.ScannerError, config.yaml.parser.ParserError): stack_trace = traceback.format_exc() + msg = "Invalid YAML. Check the traceback above for more details." raise ParameterParsingError( - "Invalid YAML. Check the traceback above for more details.", + msg, file_path, stack_trace, ) except Exception: stack_trace = traceback.format_exc() + msg = "Invalid parameter file content. Check the traceback above for more details." raise ParameterParsingError( - "Invalid parameter file content. Check the traceback above for more details.", + msg, file_path, stack_trace, ) @@ -63,32 +66,32 @@ def _load_yaml_file(file_path): def _parse_child(child_name, child, child_path): if "values" in child: return parameters.Parameter(child_name, child, child_path) - elif "brackets" in child: + if "brackets" in child: return parameters.ParameterScale(child_name, child, child_path) - elif isinstance(child, dict) and all( - [periods.INSTANT_PATTERN.match(str(key)) for key in child.keys()] + if isinstance(child, dict) and all( + periods.INSTANT_PATTERN.match(str(key)) for key in child ): return parameters.Parameter(child_name, child, child_path) - else: - return parameters.ParameterNode(child_name, data=child, file_path=child_path) + return parameters.ParameterNode(child_name, data=child, file_path=child_path) -def _set_backward_compatibility_metadata(parameter, data): +def _set_backward_compatibility_metadata(parameter, data) -> None: if data.get("unit") is not None: parameter.metadata["unit"] = data["unit"] if data.get("reference") is not None: parameter.metadata["reference"] = data["reference"] -def _validate_parameter(parameter, data, data_type=None, allowed_keys=None): +def _validate_parameter(parameter, data, data_type=None, allowed_keys=None) -> None: type_map = { dict: "object", list: "array", } if data_type is not None and not isinstance(data, data_type): + msg = f"'{parameter.name}' must be of type {type_map[data_type]}." raise ParameterParsingError( - "'{}' must be of type {}.".format(parameter.name, type_map[data_type]), + msg, parameter.file_path, ) @@ -96,9 +99,8 @@ def _validate_parameter(parameter, data, data_type=None, allowed_keys=None): keys = data.keys() for key in keys: if key not in allowed_keys: + msg = f"Unexpected property '{key}' in '{parameter.name}'. Allowed properties are {list(allowed_keys)}." raise ParameterParsingError( - "Unexpected property '{}' in '{}'. Allowed properties are {}.".format( - key, parameter.name, list(allowed_keys) - ), + msg, parameter.file_path, ) diff --git a/openfisca_core/parameters/parameter.py b/openfisca_core/parameters/parameter.py index 9c9d7e7093..528f54cccd 100644 --- a/openfisca_core/parameters/parameter.py +++ b/openfisca_core/parameters/parameter.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import Dict, List, Optional - import copy import os @@ -46,20 +44,22 @@ class Parameter(AtInstantLike): """ - def __init__(self, name: str, data: dict, file_path: Optional[str] = None) -> None: + def __init__(self, name: str, data: dict, file_path: str | None = None) -> None: self.name: str = name - self.file_path: Optional[str] = file_path + self.file_path: str | None = file_path helpers._validate_parameter(self, data, data_type=dict) - self.description: Optional[str] = None - self.metadata: Dict = {} - self.documentation: Optional[str] = None + self.description: str | None = None + self.metadata: dict = {} + self.documentation: str | None = None self.values_history = self # Only for backward compatibility # Normal parameter declaration: the values are declared under the 'values' key: parse the description and metadata. if data.get("values"): # 'unit' and 'reference' are only listed here for backward compatibility helpers._validate_parameter( - self, data, allowed_keys=config.COMMON_KEYS.union({"values"}) + self, + data, + allowed_keys=config.COMMON_KEYS.union({"values"}), ) self.description = data.get("description") @@ -75,16 +75,16 @@ def __init__(self, name: str, data: dict, file_path: Optional[str] = None) -> No values = data instants = sorted( - values.keys(), reverse=True + values.keys(), + reverse=True, ) # sort in reverse chronological order values_list = [] for instant_str in instants: if not periods.INSTANT_PATTERN.match(instant_str): + msg = f"Invalid property '{instant_str}' in '{self.name}'. Properties must be valid YYYY-MM-DD instants, such as 2017-01-15." raise ParameterParsingError( - "Invalid property '{}' in '{}'. Properties must be valid YYYY-MM-DD instants, such as 2017-01-15.".format( - instant_str, self.name - ), + msg, file_path, ) @@ -108,9 +108,9 @@ def __init__(self, name: str, data: dict, file_path: Optional[str] = None) -> No ) values_list.append(value_at_instant) - self.values_list: List[ParameterAtInstant] = values_list + self.values_list: list[ParameterAtInstant] = values_list - def __repr__(self): + def __repr__(self) -> str: return os.linesep.join( [ "{}: {}".format( @@ -118,7 +118,7 @@ def __repr__(self): value.value if value.value is not None else "null", ) for value in self.values_list - ] + ], ) def __eq__(self, other): @@ -134,9 +134,8 @@ def clone(self): ] return clone - def update(self, period=None, start=None, stop=None, value=None): - """ - Change the value for a given period. + def update(self, period=None, start=None, stop=None, value=None) -> None: + """Change the value for a given period. :param period: Period where the value is modified. If set, `start` and `stop` should be `None`. :param start: Start of the period. Instance of `openfisca_core.periods.Instant`. If set, `period` should be `None`. @@ -145,15 +144,17 @@ def update(self, period=None, start=None, stop=None, value=None): """ if period is not None: if start is not None or stop is not None: + msg = "Wrong input for 'update' method: use either 'update(period, value = value)' or 'update(start = start, stop = stop, value = value)'. You cannot both use 'period' and 'start' or 'stop'." raise TypeError( - "Wrong input for 'update' method: use either 'update(period, value = value)' or 'update(start = start, stop = stop, value = value)'. You cannot both use 'period' and 'start' or 'stop'." + msg, ) if isinstance(period, str): period = periods.period(period) start = period.start stop = period.stop if start is None: - raise ValueError("You must provide either a start or a period") + msg = "You must provide either a start or a period" + raise ValueError(msg) start_str = str(start) stop_str = str(stop.offset(1, "day")) if stop else None @@ -172,20 +173,23 @@ def update(self, period=None, start=None, stop=None, value=None): if stop_str: if new_values and (stop_str == new_values[-1].instant_str): pass # such interval is empty + elif i < n: + overlapped_value = old_values[i].value + value_name = helpers._compose_name(self.name, item_name=stop_str) + new_interval = ParameterAtInstant( + value_name, + stop_str, + data={"value": overlapped_value}, + ) + new_values.append(new_interval) else: - if i < n: - overlapped_value = old_values[i].value - value_name = helpers._compose_name(self.name, item_name=stop_str) - new_interval = ParameterAtInstant( - value_name, stop_str, data={"value": overlapped_value} - ) - new_values.append(new_interval) - else: - value_name = helpers._compose_name(self.name, item_name=stop_str) - new_interval = ParameterAtInstant( - value_name, stop_str, data={"value": None} - ) - new_values.append(new_interval) + value_name = helpers._compose_name(self.name, item_name=stop_str) + new_interval = ParameterAtInstant( + value_name, + stop_str, + data={"value": None}, + ) + new_values.append(new_interval) # Insert new interval value_name = helpers._compose_name(self.name, item_name=start_str) diff --git a/openfisca_core/parameters/parameter_at_instant.py b/openfisca_core/parameters/parameter_at_instant.py index b84dc5b2b6..ae525cf829 100644 --- a/openfisca_core/parameters/parameter_at_instant.py +++ b/openfisca_core/parameters/parameter_at_instant.py @@ -1,5 +1,3 @@ -import typing - import copy from openfisca_core import commons @@ -8,23 +6,22 @@ class ParameterAtInstant: - """ - A value of a parameter at a given instant. - """ + """A value of a parameter at a given instant.""" # 'unit' and 'reference' are only listed here for backward compatibility - _allowed_keys = set(["value", "metadata", "unit", "reference"]) + _allowed_keys = {"value", "metadata", "unit", "reference"} - def __init__(self, name, instant_str, data=None, file_path=None, metadata=None): - """ - :param str name: name of the parameter, e.g. "taxes.some_tax.some_param" + def __init__( + self, name, instant_str, data=None, file_path=None, metadata=None + ) -> None: + """:param str name: name of the parameter, e.g. "taxes.some_tax.some_param" :param str instant_str: Date of the value in the format `YYYY-MM-DD`. :param dict data: Data, usually loaded from a YAML file. """ self.name: str = name self.instant_str: str = instant_str self.file_path: str = file_path - self.metadata: typing.Dict = {} + self.metadata: dict = {} # Accept { 2015-01-01: 4000 } if not isinstance(data, dict) and isinstance(data, config.ALLOWED_PARAM_TYPES): @@ -39,21 +36,25 @@ def __init__(self, name, instant_str, data=None, file_path=None, metadata=None): helpers._set_backward_compatibility_metadata(self, data) self.metadata.update(data.get("metadata", {})) - def validate(self, data): + def validate(self, data) -> None: helpers._validate_parameter( - self, data, data_type=dict, allowed_keys=self._allowed_keys + self, + data, + data_type=dict, + allowed_keys=self._allowed_keys, ) try: value = data["value"] except KeyError: + msg = f"Missing 'value' property for {self.name}" raise ParameterParsingError( - "Missing 'value' property for {}".format(self.name), self.file_path + msg, + self.file_path, ) if not isinstance(value, config.ALLOWED_PARAM_TYPES): + msg = f"Value in {self.name} has type {type(value)}, which is not one of the allowed types ({config.ALLOWED_PARAM_TYPES}): {value}" raise ParameterParsingError( - "Value in {} has type {}, which is not one of the allowed types ({}): {}".format( - self.name, type(value), config.ALLOWED_PARAM_TYPES, value - ), + msg, self.file_path, ) @@ -64,8 +65,8 @@ def __eq__(self, other): and (self.value == other.value) ) - def __repr__(self): - return "ParameterAtInstant({})".format({self.instant_str: self.value}) + def __repr__(self) -> str: + return "ParameterAtInstant({self.instant_str: self.value})" def clone(self): clone = commons.empty_clone(self) diff --git a/openfisca_core/parameters/parameter_node.py b/openfisca_core/parameters/parameter_node.py index 6a344a09a9..2be3a9acfd 100644 --- a/openfisca_core/parameters/parameter_node.py +++ b/openfisca_core/parameters/parameter_node.py @@ -14,17 +14,14 @@ class ParameterNode(AtInstantLike): - """ - A node in the legislation `parameter tree `_. - """ + """A node in the legislation `parameter tree `_.""" - _allowed_keys: typing.Optional[ - typing.Iterable[str] - ] = None # By default, no restriction on the keys + _allowed_keys: None | (typing.Iterable[str]) = ( + None # By default, no restriction on the keys + ) - def __init__(self, name="", directory_path=None, data=None, file_path=None): - """ - Instantiate a ParameterNode either from a dict, (using `data`), or from a directory containing YAML files (using `directory_path`). + def __init__(self, name="", directory_path=None, data=None, file_path=None) -> None: + """Instantiate a ParameterNode either from a dict, (using `data`), or from a directory containing YAML files (using `directory_path`). :param str name: Name of the node, eg "taxes.some_tax". :param str directory_path: Directory containing YAML files describing the node. @@ -51,16 +48,20 @@ def __init__(self, name="", directory_path=None, data=None, file_path=None): Instantiate a ParameterNode from a directory containing YAML parameter files: - >>> node = ParameterNode('benefits', directory_path = '/path/to/country_package/parameters/benefits') + >>> node = ParameterNode( + ... "benefits", + ... directory_path="/path/to/country_package/parameters/benefits", + ... ) """ self.name: str = name - self.children: typing.Dict[ - str, typing.Union[ParameterNode, Parameter, parameters.ParameterScale] + self.children: dict[ + str, + ParameterNode | Parameter | parameters.ParameterScale, ] = {} self.description: str = None self.documentation: str = None self.file_path: str = None - self.metadata: typing.Dict = {} + self.metadata: dict = {} if directory_path: self.file_path = directory_path @@ -76,7 +77,9 @@ def __init__(self, name="", directory_path=None, data=None, file_path=None): if child_name == "index": data = helpers._load_yaml_file(child_path) or {} helpers._validate_parameter( - self, data, allowed_keys=config.COMMON_KEYS + self, + data, + allowed_keys=config.COMMON_KEYS, ) self.description = data.get("description") self.documentation = data.get("documentation") @@ -85,7 +88,8 @@ def __init__(self, name="", directory_path=None, data=None, file_path=None): else: child_name_expanded = helpers._compose_name(name, child_name) child = helpers.load_parameter_file( - child_path, child_name_expanded + child_path, + child_name_expanded, ) self.add_child(child_name, child) @@ -93,14 +97,18 @@ def __init__(self, name="", directory_path=None, data=None, file_path=None): child_name = os.path.basename(child_path) child_name_expanded = helpers._compose_name(name, child_name) child = ParameterNode( - child_name_expanded, directory_path=child_path + child_name_expanded, + directory_path=child_path, ) self.add_child(child_name, child) else: self.file_path = file_path helpers._validate_parameter( - self, data, data_type=dict, allowed_keys=self._allowed_keys + self, + data, + data_type=dict, + allowed_keys=self._allowed_keys, ) self.description = data.get("description") self.documentation = data.get("documentation") @@ -115,50 +123,43 @@ def __init__(self, name="", directory_path=None, data=None, file_path=None): child = helpers._parse_child(child_name_expanded, child, file_path) self.add_child(child_name, child) - def merge(self, other): - """ - Merges another ParameterNode into the current node. + def merge(self, other) -> None: + """Merges another ParameterNode into the current node. In case of child name conflict, the other node child will replace the current node child. """ for child_name, child in other.children.items(): self.add_child(child_name, child) - def add_child(self, name, child): - """ - Add a new child to the node. + def add_child(self, name, child) -> None: + """Add a new child to the node. :param name: Name of the child that must be used to access that child. Should not contain anything that could interfere with the operator `.` (dot). :param child: The new child, an instance of :class:`.ParameterScale` or :class:`.Parameter` or :class:`.ParameterNode`. """ if name in self.children: - raise ValueError("{} has already a child named {}".format(self.name, name)) + msg = f"{self.name} has already a child named {name}" + raise ValueError(msg) if not ( - isinstance(child, ParameterNode) - or isinstance(child, Parameter) - or isinstance(child, parameters.ParameterScale) + isinstance(child, (ParameterNode, Parameter, parameters.ParameterScale)) ): + msg = f"child must be of type ParameterNode, Parameter, or Scale. Instead got {type(child)}" raise TypeError( - "child must be of type ParameterNode, Parameter, or Scale. Instead got {}".format( - type(child) - ) + msg, ) self.children[name] = child setattr(self, name, child) - def __repr__(self): - result = os.linesep.join( + def __repr__(self) -> str: + return os.linesep.join( [ os.linesep.join(["{}:", "{}"]).format(name, tools.indent(repr(value))) for name, value in sorted(self.children.items()) - ] + ], ) - return result def get_descendants(self): - """ - Return a generator containing all the parameters and nodes recursively contained in this `ParameterNode` - """ + """Return a generator containing all the parameters and nodes recursively contained in this `ParameterNode`.""" for child in self.children.values(): yield child yield from child.get_descendants() diff --git a/openfisca_core/parameters/parameter_node_at_instant.py b/openfisca_core/parameters/parameter_node_at_instant.py index d98d88a698..b66c0c1ed7 100644 --- a/openfisca_core/parameters/parameter_node_at_instant.py +++ b/openfisca_core/parameters/parameter_node_at_instant.py @@ -1,5 +1,4 @@ import os -import sys import numpy @@ -9,17 +8,13 @@ class ParameterNodeAtInstant: - """ - Parameter node of the legislation, at a given instant. - """ + """Parameter node of the legislation, at a given instant.""" - def __init__(self, name, node, instant_str): - """ - :param name: Name of the node. + def __init__(self, name, node, instant_str) -> None: + """:param name: Name of the node. :param node: Original :any:`ParameterNode` instance. :param instant_str: A date in the format `YYYY-MM-DD`. """ - # The "technical" attributes are hidden, so that the node children can be easily browsed with auto-completion without pollution self._name = name self._instant_str = instant_str @@ -30,7 +25,7 @@ def __init__(self, name, node, instant_str): if child_at_instant is not None: self.add_child(child_name, child_at_instant) - def add_child(self, child_name, child_at_instant): + def add_child(self, child_name, child_at_instant) -> None: self._children[child_name] = child_at_instant setattr(self, child_name, child_at_instant) @@ -45,7 +40,7 @@ def __getitem__(self, key): if numpy.issubdtype(key.dtype, numpy.datetime64): return ( parameters.VectorialAsofDateParameterNodeAtInstant.build_from_node( - self + self, )[key] ) @@ -55,13 +50,10 @@ def __getitem__(self, key): def __iter__(self): return iter(self._children) - def __repr__(self): - result = os.linesep.join( + def __repr__(self) -> str: + return os.linesep.join( [ os.linesep.join(["{}:", "{}"]).format(name, tools.indent(repr(value))) for name, value in self._children.items() - ] + ], ) - if sys.version_info < (3, 0): - return result - return result diff --git a/openfisca_core/parameters/parameter_scale.py b/openfisca_core/parameters/parameter_scale.py index f3d636ed0c..b01b6a372a 100644 --- a/openfisca_core/parameters/parameter_scale.py +++ b/openfisca_core/parameters/parameter_scale.py @@ -1,5 +1,3 @@ -import typing - import copy import os @@ -15,34 +13,33 @@ class ParameterScale(AtInstantLike): - """ - A parameter scale (for instance a marginal scale). - """ + """A parameter scale (for instance a marginal scale).""" # 'unit' and 'reference' are only listed here for backward compatibility _allowed_keys = config.COMMON_KEYS.union({"brackets"}) - def __init__(self, name, data, file_path): - """ - :param name: name of the scale, eg "taxes.some_scale" + def __init__(self, name, data, file_path) -> None: + """:param name: name of the scale, eg "taxes.some_scale" :param data: Data loaded from a YAML file. In case of a reform, the data can also be created dynamically. :param file_path: File the parameter was loaded from. """ self.name: str = name self.file_path: str = file_path helpers._validate_parameter( - self, data, data_type=dict, allowed_keys=self._allowed_keys + self, + data, + data_type=dict, + allowed_keys=self._allowed_keys, ) self.description: str = data.get("description") - self.metadata: typing.Dict = {} + self.metadata: dict = {} helpers._set_backward_compatibility_metadata(self, data) self.metadata.update(data.get("metadata", {})) if not isinstance(data.get("brackets", []), list): + msg = f"Property 'brackets' of scale '{self.name}' must be of type array." raise ParameterParsingError( - "Property 'brackets' of scale '{}' must be of type array.".format( - self.name - ), + msg, self.file_path, ) @@ -50,24 +47,25 @@ def __init__(self, name, data, file_path): for i, bracket_data in enumerate(data.get("brackets", [])): bracket_name = helpers._compose_name(name, item_name=i) bracket = parameters.ParameterScaleBracket( - name=bracket_name, data=bracket_data, file_path=file_path + name=bracket_name, + data=bracket_data, + file_path=file_path, ) brackets.append(bracket) - self.brackets: typing.List[parameters.ParameterScaleBracket] = brackets + self.brackets: list[parameters.ParameterScaleBracket] = brackets def __getitem__(self, key): if isinstance(key, int) and key < len(self.brackets): return self.brackets[key] - else: - raise KeyError(key) + raise KeyError(key) - def __repr__(self): + def __repr__(self) -> str: return os.linesep.join( ["brackets:"] + [ tools.indent("-" + tools.indent(repr(bracket))[1:]) for bracket in self.brackets - ] + ], ) def get_descendants(self): @@ -93,7 +91,7 @@ def _get_at_instant(self, instant): threshold = bracket.threshold scale.add_bracket(threshold, amount) return scale - elif any("amount" in bracket._children for bracket in brackets): + if any("amount" in bracket._children for bracket in brackets): scale = MarginalAmountTaxScale() for bracket in brackets: if "amount" in bracket._children and "threshold" in bracket._children: @@ -101,7 +99,7 @@ def _get_at_instant(self, instant): threshold = bracket.threshold scale.add_bracket(threshold, amount) return scale - elif any("average_rate" in bracket._children for bracket in brackets): + if any("average_rate" in bracket._children for bracket in brackets): scale = LinearAverageRateTaxScale() for bracket in brackets: @@ -113,12 +111,11 @@ def _get_at_instant(self, instant): threshold = bracket.threshold scale.add_bracket(threshold, average_rate) return scale - else: - scale = MarginalRateTaxScale() - - for bracket in brackets: - if "rate" in bracket._children and "threshold" in bracket._children: - rate = bracket.rate - threshold = bracket.threshold - scale.add_bracket(threshold, rate) - return scale + scale = MarginalRateTaxScale() + + for bracket in brackets: + if "rate" in bracket._children and "threshold" in bracket._children: + rate = bracket.rate + threshold = bracket.threshold + scale.add_bracket(threshold, rate) + return scale diff --git a/openfisca_core/parameters/parameter_scale_bracket.py b/openfisca_core/parameters/parameter_scale_bracket.py index 2e3e65e649..b9691ea3ca 100644 --- a/openfisca_core/parameters/parameter_scale_bracket.py +++ b/openfisca_core/parameters/parameter_scale_bracket.py @@ -2,8 +2,6 @@ class ParameterScaleBracket(ParameterNode): - """ - A parameter scale bracket. - """ + """A parameter scale bracket.""" - _allowed_keys = set(["amount", "threshold", "rate", "average_rate"]) + _allowed_keys = {"amount", "threshold", "rate", "average_rate"} diff --git a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py index e00ce11733..27be1f6946 100644 --- a/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_asof_date_parameter_node_at_instant.py @@ -7,9 +7,8 @@ class VectorialAsofDateParameterNodeAtInstant(VectorialParameterNodeAtInstant): - """ - Parameter node of the legislation at a given instant which has been vectorized along some date. - Vectorized parameters allow requests such as parameters.housing_benefit[date], where date is a np.datetime64 type vector + """Parameter node of the legislation at a given instant which has been vectorized along some date. + Vectorized parameters allow requests such as parameters.housing_benefit[date], where date is a numpy.datetime64 type vector. """ @staticmethod @@ -19,13 +18,15 @@ def build_from_node(node): # Recursively vectorize the children of the node vectorial_subnodes = tuple( [ - VectorialAsofDateParameterNodeAtInstant.build_from_node( - node[subnode_name] - ).vector - if isinstance(node[subnode_name], ParameterNodeAtInstant) - else node[subnode_name] + ( + VectorialAsofDateParameterNodeAtInstant.build_from_node( + node[subnode_name], + ).vector + if isinstance(node[subnode_name], ParameterNodeAtInstant) + else node[subnode_name] + ) for subnode_name in subnodes_name - ] + ], ) # A vectorial node is a wrapper around a numpy recarray # We first build the recarray @@ -40,7 +41,9 @@ def build_from_node(node): ], ) return VectorialAsofDateParameterNodeAtInstant( - node._name, recarray.view(numpy.recarray), node._instant_str + node._name, + recarray.view(numpy.recarray), + node._instant_str, ) def __getitem__(self, key): @@ -49,12 +52,12 @@ def __getitem__(self, key): key = numpy.array([key], dtype="datetime64[D]") return self.__getattr__(key) # If the key is a vector, e.g. ['1990-11-25', '1983-04-17', '1969-09-09'] - elif isinstance(key, numpy.ndarray): + if isinstance(key, numpy.ndarray): assert numpy.issubdtype(key.dtype, numpy.datetime64) names = list( - self.dtype.names + self.dtype.names, ) # Get all the names of the subnodes, e.g. ['before_X', 'after_X', 'after_Y'] - values = numpy.asarray([value for value in self.vector[0]]) + values = numpy.asarray(list(self.vector[0])) names = [name for name in names if not name.startswith("before")] names = [ numpy.datetime64("-".join(name[len("after_") :].split("_"))) @@ -65,10 +68,14 @@ def __getitem__(self, key): # If the result is not a leaf, wrap the result in a vectorial node. if numpy.issubdtype(result.dtype, numpy.record) or numpy.issubdtype( - result.dtype, numpy.void + result.dtype, + numpy.void, ): return VectorialAsofDateParameterNodeAtInstant( - self._name, result.view(numpy.recarray), self._instant_str + self._name, + result.view(numpy.recarray), + self._instant_str, ) return result + return None diff --git a/openfisca_core/parameters/vectorial_parameter_node_at_instant.py b/openfisca_core/parameters/vectorial_parameter_node_at_instant.py index 0681848cfa..74cd02d378 100644 --- a/openfisca_core/parameters/vectorial_parameter_node_at_instant.py +++ b/openfisca_core/parameters/vectorial_parameter_node_at_instant.py @@ -1,3 +1,5 @@ +from typing import NoReturn + import numpy from openfisca_core import parameters @@ -7,9 +9,8 @@ class VectorialParameterNodeAtInstant: - """ - Parameter node of the legislation at a given instant which has been vectorized. - Vectorized parameters allow requests such as parameters.housing_benefit[zipcode], where zipcode is a vector + """Parameter node of the legislation at a given instant which has been vectorized. + Vectorized parameters allow requests such as parameters.housing_benefit[zipcode], where zipcode is a vector. """ @staticmethod @@ -19,13 +20,15 @@ def build_from_node(node): # Recursively vectorize the children of the node vectorial_subnodes = tuple( [ - VectorialParameterNodeAtInstant.build_from_node( - node[subnode_name] - ).vector - if isinstance(node[subnode_name], parameters.ParameterNodeAtInstant) - else node[subnode_name] + ( + VectorialParameterNodeAtInstant.build_from_node( + node[subnode_name], + ).vector + if isinstance(node[subnode_name], parameters.ParameterNodeAtInstant) + else node[subnode_name] + ) for subnode_name in subnodes_name - ] + ], ) # A vectorial node is a wrapper around a numpy recarray # We first build the recarray @@ -41,45 +44,33 @@ def build_from_node(node): ) return VectorialParameterNodeAtInstant( - node._name, recarray.view(numpy.recarray), node._instant_str + node._name, + recarray.view(numpy.recarray), + node._instant_str, ) @staticmethod - def check_node_vectorisable(node): - """ - Check that a node can be casted to a vectorial node, in order to be able to use fancy indexing. - """ + def check_node_vectorisable(node) -> None: + """Check that a node can be casted to a vectorial node, in order to be able to use fancy indexing.""" MESSAGE_PART_1 = "Cannot use fancy indexing on parameter node '{}', as" MESSAGE_PART_3 = ( "To use fancy indexing on parameter node, its children must be homogenous." ) MESSAGE_PART_4 = "See more at ." - def raise_key_inhomogeneity_error(node_with_key, node_without_key, missing_key): - message = " ".join( - [ - MESSAGE_PART_1, - "'{}' exists, but '{}' doesn't.", - MESSAGE_PART_3, - MESSAGE_PART_4, - ] - ).format( + def raise_key_inhomogeneity_error( + node_with_key, node_without_key, missing_key + ) -> NoReturn: + message = f"{MESSAGE_PART_1} '{{}}' exists, but '{{}}' doesn't. {MESSAGE_PART_3} {MESSAGE_PART_4}".format( node._name, - ".".join([node_with_key, missing_key]), - ".".join([node_without_key, missing_key]), + f"{node_with_key}.{missing_key}", + f"{node_without_key}.{missing_key}", ) raise ValueError(message) - def raise_type_inhomogeneity_error(node_name, non_node_name): - message = " ".join( - [ - MESSAGE_PART_1, - "'{}' is a node, but '{}' is not.", - MESSAGE_PART_3, - MESSAGE_PART_4, - ] - ).format( + def raise_type_inhomogeneity_error(node_name, non_node_name) -> NoReturn: + message = f"{MESSAGE_PART_1} '{{}}' is a node, but '{{}}' is not. {MESSAGE_PART_3} {MESSAGE_PART_4}".format( node._name, node_name, non_node_name, @@ -87,14 +78,8 @@ def raise_type_inhomogeneity_error(node_name, non_node_name): raise ValueError(message) - def raise_not_implemented(node_name, node_type): - message = " ".join( - [ - MESSAGE_PART_1, - "'{}' is a '{}', and fancy indexing has not been implemented yet on this kind of parameters.", - MESSAGE_PART_4, - ] - ).format( + def raise_not_implemented(node_name, node_type) -> NoReturn: + message = f"{MESSAGE_PART_1} '{{}}' is a '{{}}', and fancy indexing has not been implemented yet on this kind of parameters. {MESSAGE_PART_4}".format( node._name, node_name, node_type, @@ -103,14 +88,11 @@ def raise_not_implemented(node_name, node_type): def extract_named_children(node): return { - ".".join([node._name, key]): value - for key, value in node._children.items() + f"{node._name}.{key}": value for key, value in node._children.items() } - def check_nodes_homogeneous(named_nodes): - """ - Check than several nodes (or parameters, or baremes) have the same structure. - """ + def check_nodes_homogeneous(named_nodes) -> None: + """Check than several nodes (or parameters, or baremes) have the same structure.""" names = list(named_nodes.keys()) nodes = list(named_nodes.values()) first_node = nodes[0] @@ -122,11 +104,13 @@ def check_nodes_homogeneous(named_nodes): raise_type_inhomogeneity_error(first_name, name) first_node_keys = first_node._children.keys() node_keys = node._children.keys() - if not first_node_keys == node_keys: + if first_node_keys != node_keys: missing_keys = set(first_node_keys).difference(node_keys) if missing_keys: # If the first_node has a key that node hasn't raise_key_inhomogeneity_error( - first_name, name, missing_keys.pop() + first_name, + name, + missing_keys.pop(), ) else: # If If the node has a key that first_node doesn't have missing_key = ( @@ -135,9 +119,9 @@ def check_nodes_homogeneous(named_nodes): raise_key_inhomogeneity_error(name, first_name, missing_key) children.update(extract_named_children(node)) check_nodes_homogeneous(children) - elif isinstance(first_node, float) or isinstance(first_node, int): + elif isinstance(first_node, (float, int)): for node, name in list(zip(nodes, names))[1:]: - if isinstance(node, int) or isinstance(node, float): + if isinstance(node, (int, float)): pass elif isinstance(node, parameters.ParameterNodeAtInstant): raise_type_inhomogeneity_error(name, first_name) @@ -149,7 +133,7 @@ def check_nodes_homogeneous(named_nodes): check_nodes_homogeneous(extract_named_children(node)) - def __init__(self, name, vector, instant_str): + def __init__(self, name, vector, instant_str) -> None: self.vector = vector self._name = name self._instant_str = instant_str @@ -165,13 +149,14 @@ def __getitem__(self, key): if isinstance(key, str): return self.__getattr__(key) # If the key is a vector, e.g. ['zone_1', 'zone_2', 'zone_1'] - elif isinstance(key, numpy.ndarray): + if isinstance(key, numpy.ndarray): if not numpy.issubdtype(key.dtype, numpy.str_): # In case the key is not a string vector, stringify it if key.dtype == object and issubclass(type(key[0]), Enum): enum = type(key[0]) key = numpy.select( - [key == item for item in enum], [item.name for item in enum] + [key == item for item in enum], + [item.name for item in enum], ) elif isinstance(key, EnumArray): enum = key.possible_values @@ -182,26 +167,33 @@ def __getitem__(self, key): else: key = key.astype("str") names = list( - self.dtype.names + self.dtype.names, ) # Get all the names of the subnodes, e.g. ['zone_1', 'zone_2'] default = numpy.full_like( - self.vector[key[0]], numpy.nan + self.vector[key[0]], + numpy.nan, ) # In case of unexpected key, we will set the corresponding value to NaN. conditions = [key == name for name in names] values = [self.vector[name] for name in names] result = numpy.select(conditions, values, default) if helpers.contains_nan(result): unexpected_key = set(key).difference(self.vector.dtype.names).pop() + msg = f"{self._name}.{unexpected_key}" raise ParameterNotFoundError( - ".".join([self._name, unexpected_key]), self._instant_str + msg, + self._instant_str, ) # If the result is not a leaf, wrap the result in a vectorial node. if numpy.issubdtype(result.dtype, numpy.record) or numpy.issubdtype( - result.dtype, numpy.void + result.dtype, + numpy.void, ): return VectorialParameterNodeAtInstant( - self._name, result.view(numpy.recarray), self._instant_str + self._name, + result.view(numpy.recarray), + self._instant_str, ) return result + return None diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index 64b2077831..95a17fb041 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -29,7 +29,6 @@ def _parse_period(value: str) -> Optional[Period]: Period((, Instant((2022, 1, 16)), 1)) """ - # If it's a complex period, next! if len(value.split(":")) != 1: return None @@ -77,26 +76,22 @@ def _parse_unit(value: str) -> DateUnit: """ - length = len(value.split("-")) isweek = value.find("W") != -1 if length == 1: return DateUnit.YEAR - elif length == 2: + if length == 2: if isweek: return DateUnit.WEEK - else: - return DateUnit.MONTH + return DateUnit.MONTH - elif length == 3: + if length == 3: if isweek: return DateUnit.WEEKDAY - else: - return DateUnit.DAY + return DateUnit.DAY - else: - raise ValueError + raise ValueError diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/config.py index 17807160e4..26ce30a5aa 100644 --- a/openfisca_core/periods/config.py +++ b/openfisca_core/periods/config.py @@ -12,11 +12,11 @@ # Matches "2015", "2015-01", "2015-01-01" # Does not match "2015-13", "2015-12-32" INSTANT_PATTERN = re.compile( - r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$" + r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$", ) date_by_instant_cache: dict = {} str_by_instant_cache: dict = {} year_or_month_or_day_re = re.compile( - r"(18|19|20)\d{2}(-(0?[1-9]|1[0-2])(-([0-2]?\d|3[0-1]))?)?$" + r"(18|19|20)\d{2}(-(0?[1-9]|1[0-2])(-([0-2]?\d|3[0-1]))?)?$", ) diff --git a/openfisca_core/periods/date_unit.py b/openfisca_core/periods/date_unit.py index a813211495..61f7fbc66f 100644 --- a/openfisca_core/periods/date_unit.py +++ b/openfisca_core/periods/date_unit.py @@ -7,7 +7,7 @@ class DateUnitMeta(EnumMeta): @property - def isoformat(self) -> tuple[DateUnit, ...]: + def isoformat(cls) -> tuple[DateUnit, ...]: """Creates a :obj:`tuple` of ``key`` with isoformat items. Returns: @@ -24,11 +24,10 @@ def isoformat(self) -> tuple[DateUnit, ...]: False """ - return DateUnit.DAY, DateUnit.MONTH, DateUnit.YEAR @property - def isocalendar(self) -> tuple[DateUnit, ...]: + def isocalendar(cls) -> tuple[DateUnit, ...]: """Creates a :obj:`tuple` of ``key`` with isocalendar items. Returns: @@ -45,7 +44,6 @@ def isocalendar(self) -> tuple[DateUnit, ...]: False """ - return DateUnit.WEEKDAY, DateUnit.WEEK, DateUnit.YEAR diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 2ce4e0cd35..c1ccc4a3a2 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -48,15 +48,15 @@ def instant(instant) -> Optional[Instant]: Instant((2021, 1, 1)) """ - if instant is None: return None if isinstance(instant, Instant): return instant if isinstance(instant, str): if not config.INSTANT_PATTERN.match(instant): + msg = f"'{instant}' is not a valid instant. Instants are described using the 'YYYY-MM-DD' format, for instance '2015-06-15'." raise ValueError( - f"'{instant}' is not a valid instant. Instants are described using the 'YYYY-MM-DD' format, for instance '2015-06-15'." + msg, ) instant = Instant(int(fragment) for fragment in instant.split("-", 2)[:3]) elif isinstance(instant, datetime.date): @@ -93,7 +93,6 @@ def instant_date(instant: Optional[Instant]) -> Optional[datetime.date]: Date(2021, 1, 1) """ - if instant is None: return None @@ -150,7 +149,6 @@ def period(value) -> Period: """ - if isinstance(value, Period): return value @@ -171,7 +169,7 @@ def period(value) -> Period: DateUnit.ETERNITY, instant(datetime.date.min), float("inf"), - ) + ), ) # For example ``2021`` gives @@ -256,13 +254,12 @@ def _raise_error(value: str) -> NoReturn: .", - ] + ], ) raise ValueError(message) @@ -290,7 +287,6 @@ def key_period_size(period: Period) -> str: '300_3' """ - unit, start, size = period return f"{unit_weight(unit)}_{size}" @@ -304,7 +300,6 @@ def unit_weights() -> dict[str, int]: {: 100, ...ETERNITY: 'eternity'>: 400} """ - return { DateUnit.WEEKDAY: 100, DateUnit.WEEK: 200, @@ -323,5 +318,4 @@ def unit_weight(unit: str) -> int: 100 """ - return unit_weights()[unit] diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 9d0893ba41..5042209492 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -78,10 +78,10 @@ class Instant(tuple): """ - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({super().__repr__()})" - def __str__(self): + def __str__(self) -> str: instant_str = config.str_by_instant_cache.get(self) if instant_str is None: @@ -135,7 +135,6 @@ def offset(self, offset, unit): Instant((2019, 12, 29)) """ - year, month, day = self assert unit in ( @@ -146,52 +145,55 @@ def offset(self, offset, unit): if unit == DateUnit.YEAR: return self.__class__((year, 1, 1)) - elif unit == DateUnit.MONTH: + if unit == DateUnit.MONTH: return self.__class__((year, month, 1)) - elif unit == DateUnit.WEEK: + if unit == DateUnit.WEEK: date = self.date date = date.start_of("week") return self.__class__((date.year, date.month, date.day)) + return None - elif offset == "last-of": + if offset == "last-of": if unit == DateUnit.YEAR: return self.__class__((year, 12, 31)) - elif unit == DateUnit.MONTH: + if unit == DateUnit.MONTH: date = self.date date = date.end_of("month") return self.__class__((date.year, date.month, date.day)) - elif unit == DateUnit.WEEK: + if unit == DateUnit.WEEK: date = self.date date = date.end_of("week") return self.__class__((date.year, date.month, date.day)) - - else: - assert isinstance( - offset, int - ), f"Invalid offset: {offset} of type {type(offset)}" - - if unit == DateUnit.YEAR: - date = self.date - date = date.add(years=offset) - return self.__class__((date.year, date.month, date.day)) - - elif unit == DateUnit.MONTH: - date = self.date - date = date.add(months=offset) - return self.__class__((date.year, date.month, date.day)) - - elif unit == DateUnit.WEEK: - date = self.date - date = date.add(weeks=offset) - return self.__class__((date.year, date.month, date.day)) - - elif unit in (DateUnit.DAY, DateUnit.WEEKDAY): - date = self.date - date = date.add(days=offset) - return self.__class__((date.year, date.month, date.day)) + return None + + assert isinstance( + offset, + int, + ), f"Invalid offset: {offset} of type {type(offset)}" + + if unit == DateUnit.YEAR: + date = self.date + date = date.add(years=offset) + return self.__class__((date.year, date.month, date.day)) + + if unit == DateUnit.MONTH: + date = self.date + date = date.add(months=offset) + return self.__class__((date.year, date.month, date.day)) + + if unit == DateUnit.WEEK: + date = self.date + date = date.add(weeks=offset) + return self.__class__((date.year, date.month, date.day)) + + if unit in (DateUnit.DAY, DateUnit.WEEKDAY): + date = self.date + date = date.add(days=offset) + return self.__class__((date.year, date.month, date.day)) + return None @property def year(self): diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 11a7b671b4..0dcf960bbf 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,7 +1,6 @@ from __future__ import annotations import typing -from collections.abc import Sequence import calendar import datetime @@ -13,6 +12,8 @@ from .instant_ import Instant if typing.TYPE_CHECKING: + from collections.abc import Sequence + from pendulum.datetime import Date @@ -141,9 +142,8 @@ def __str__(self) -> str: if month == 1: # civil year starting from january return str(f_year) - else: - # rolling year - return f"{DateUnit.YEAR}:{f_year}-{month:02d}" + # rolling year + return f"{DateUnit.YEAR}:{f_year}-{month:02d}" # simple month if unit == DateUnit.MONTH and size == 1: @@ -156,8 +156,7 @@ def __str__(self) -> str: if unit == DateUnit.DAY: if size == 1: return f"{f_year}-{month:02d}-{day:02d}" - else: - return f"{unit}:{f_year}-{month:02d}-{day:02d}:{size}" + return f"{unit}:{f_year}-{month:02d}-{day:02d}:{size}" # 1 week if unit == DateUnit.WEEK and size == 1: @@ -201,7 +200,6 @@ def unit(self) -> str: """ - return self[0] @property @@ -215,7 +213,6 @@ def start(self) -> Instant: Instant((2021, 10, 1)) """ - return self[1] @property @@ -229,7 +226,6 @@ def size(self) -> int: 3 """ - return self[2] @property @@ -249,9 +245,9 @@ def date(self) -> Date: ValueError: "date" is undefined for a period of size > 1: year:2021-10:3. """ - if self.size != 1: - raise ValueError(f'"date" is undefined for a period of size > 1: {self}.') + msg = f'"date" is undefined for a period of size > 1: {self}.' + raise ValueError(msg) return self.start.date @@ -272,11 +268,11 @@ def size_in_years(self) -> int: ValueError: Can't calculate number of years in a month. """ - if self.unit == DateUnit.YEAR: return self.size - raise ValueError(f"Can't calculate number of years in a {self.unit}.") + msg = f"Can't calculate number of years in a {self.unit}." + raise ValueError(msg) @property def size_in_months(self) -> int: @@ -295,14 +291,14 @@ def size_in_months(self) -> int: ValueError: Can't calculate number of months in a day. """ - if self.unit == DateUnit.YEAR: return self.size * 12 if self.unit == DateUnit.MONTH: return self.size - raise ValueError(f"Can't calculate number of months in a {self.unit}.") + msg = f"Can't calculate number of months in a {self.unit}." + raise ValueError(msg) @property def size_in_days(self) -> int: @@ -320,7 +316,6 @@ def size_in_days(self) -> int: 92 """ - if self.unit in (DateUnit.YEAR, DateUnit.MONTH): last_day = self.start.offset(self.size, self.unit).offset(-1, DateUnit.DAY) return (last_day.date - self.start.date).days + 1 @@ -331,7 +326,8 @@ def size_in_days(self) -> int: if self.unit in (DateUnit.DAY, DateUnit.WEEKDAY): return self.size - raise ValueError(f"Can't calculate number of days in a {self.unit}.") + msg = f"Can't calculate number of days in a {self.unit}." + raise ValueError(msg) @property def size_in_weeks(self): @@ -349,7 +345,6 @@ def size_in_weeks(self): 261 """ - if self.unit == DateUnit.YEAR: start = self.start.date cease = start.add(years=self.size) @@ -365,7 +360,8 @@ def size_in_weeks(self): if self.unit == DateUnit.WEEK: return self.size - raise ValueError(f"Can't calculate number of weeks in a {self.unit}.") + msg = f"Can't calculate number of weeks in a {self.unit}." + raise ValueError(msg) @property def size_in_weekdays(self): @@ -383,7 +379,6 @@ def size_in_weekdays(self): 21 """ - if self.unit == DateUnit.YEAR: return self.size_in_weeks * 7 @@ -397,7 +392,8 @@ def size_in_weekdays(self): if self.unit in (DateUnit.DAY, DateUnit.WEEKDAY): return self.size - raise ValueError(f"Can't calculate number of weekdays in a {self.unit}.") + msg = f"Can't calculate number of weekdays in a {self.unit}." + raise ValueError(msg) @property def days(self): @@ -430,7 +426,7 @@ def intersection(self, start, stop): DateUnit.YEAR, intersection_start, intersection_stop.year - intersection_start.year + 1, - ) + ), ) if ( intersection_start.day == 1 @@ -447,14 +443,14 @@ def intersection(self, start, stop): - intersection_start.month + 1 ), - ) + ), ) return self.__class__( ( DateUnit.DAY, intersection_start, (intersection_stop.date - intersection_start.date).days + 1, - ) + ), ) def get_subperiods(self, unit: DateUnit) -> Sequence[Period]: @@ -470,9 +466,9 @@ def get_subperiods(self, unit: DateUnit) -> Sequence[Period]: [Period((, Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] """ - if helpers.unit_weight(self.unit) < helpers.unit_weight(unit): - raise ValueError(f"Cannot subdivide {self.unit} into {unit}") + msg = f"Cannot subdivide {self.unit} into {unit}" + raise ValueError(msg) if unit == DateUnit.YEAR: return [self.this_year.offset(i, DateUnit.YEAR) for i in range(self.size)] @@ -500,7 +496,8 @@ def get_subperiods(self, unit: DateUnit) -> Sequence[Period]: for i in range(self.size_in_weekdays) ] - raise ValueError(f"Cannot subdivide {self.unit} into {unit}") + msg = f"Cannot subdivide {self.unit} into {unit}" + raise ValueError(msg) def offset(self, offset, unit=None): """Increment (or decrement) the given period with offset units. @@ -524,7 +521,9 @@ def offset(self, offset, unit=None): >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset(1, DateUnit.DAY) Period((, Instant((2021, 1, 2)), 12)) - >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset(1, DateUnit.MONTH) + >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset( + ... 1, DateUnit.MONTH + ... ) Period((, Instant((2021, 2, 1)), 12)) >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset(1, DateUnit.YEAR) @@ -578,110 +577,157 @@ def offset(self, offset, unit=None): >>> Period((DateUnit.YEAR, Instant((2014, 1, 1)), 1)).offset(-3) Period((, Instant((2011, 1, 1)), 1)) - >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 1)).offset("first-of", DateUnit.MONTH) + >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 1)).offset( + ... "first-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 1)), 1)) - >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 1)).offset("first-of", DateUnit.YEAR) + >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 1)).offset( + ... "first-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 1, 1)), 1)) - >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 4)).offset("first-of", DateUnit.MONTH) + >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 4)).offset( + ... "first-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 1)), 4)) - >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 4)).offset("first-of", DateUnit.YEAR) + >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 4)).offset( + ... "first-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 1, 1)), 4)) >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset("first-of") Period((, Instant((2014, 2, 1)), 1)) - >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset("first-of", DateUnit.MONTH) + >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset( + ... "first-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 1)), 1)) - >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset("first-of", DateUnit.YEAR) + >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset( + ... "first-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 1, 1)), 1)) >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset("first-of") Period((, Instant((2014, 2, 1)), 4)) - >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset("first-of", DateUnit.MONTH) + >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset( + ... "first-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 1)), 4)) - >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset("first-of", DateUnit.YEAR) + >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset( + ... "first-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 1, 1)), 4)) >>> Period((DateUnit.YEAR, Instant((2014, 1, 30)), 1)).offset("first-of") Period((, Instant((2014, 1, 1)), 1)) - >>> Period((DateUnit.YEAR, Instant((2014, 1, 30)), 1)).offset("first-of", DateUnit.MONTH) + >>> Period((DateUnit.YEAR, Instant((2014, 1, 30)), 1)).offset( + ... "first-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 1, 1)), 1)) - >>> Period((DateUnit.YEAR, Instant((2014, 1, 30)), 1)).offset("first-of", DateUnit.YEAR) + >>> Period((DateUnit.YEAR, Instant((2014, 1, 30)), 1)).offset( + ... "first-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 1, 1)), 1)) >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset("first-of") Period((, Instant((2014, 1, 1)), 1)) - >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset("first-of", DateUnit.MONTH) + >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset( + ... "first-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 1)), 1)) - >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset("first-of", DateUnit.YEAR) + >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset( + ... "first-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 1, 1)), 1)) - >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 1)).offset("last-of", DateUnit.MONTH) + >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 1)).offset( + ... "last-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 28)), 1)) - >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 1)).offset("last-of", DateUnit.YEAR) + >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 1)).offset( + ... "last-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 12, 31)), 1)) - >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 4)).offset("last-of", DateUnit.MONTH) + >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 4)).offset( + ... "last-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 28)), 4)) - >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 4)).offset("last-of", DateUnit.YEAR) + >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 4)).offset( + ... "last-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 12, 31)), 4)) >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset("last-of") Period((, Instant((2014, 2, 28)), 1)) - >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset("last-of", DateUnit.MONTH) + >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset( + ... "last-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 28)), 1)) - >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset("last-of", DateUnit.YEAR) + >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 1)).offset( + ... "last-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 12, 31)), 1)) >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset("last-of") Period((, Instant((2014, 2, 28)), 4)) - >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset("last-of", DateUnit.MONTH) + >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset( + ... "last-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 28)), 4)) - >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset("last-of", DateUnit.YEAR) + >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset( + ... "last-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 12, 31)), 4)) >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset("last-of") Period((, Instant((2014, 12, 31)), 1)) - >>> Period((DateUnit.YEAR, Instant((2014, 1, 1)), 1)).offset("last-of", DateUnit.MONTH) + >>> Period((DateUnit.YEAR, Instant((2014, 1, 1)), 1)).offset( + ... "last-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 1, 31)), 1)) - >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset("last-of", DateUnit.YEAR) + >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset( + ... "last-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 12, 31)), 1)) >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset("last-of") Period((, Instant((2014, 12, 31)), 1)) - >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset("last-of", DateUnit.MONTH) + >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset( + ... "last-of", DateUnit.MONTH + ... ) Period((, Instant((2014, 2, 28)), 1)) - >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset("last-of", DateUnit.YEAR) + >>> Period((DateUnit.YEAR, Instant((2014, 2, 3)), 1)).offset( + ... "last-of", DateUnit.YEAR + ... ) Period((, Instant((2014, 12, 31)), 1)) """ - return self.__class__( ( self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2], - ) + ), ) def contains(self, other: Period) -> bool: @@ -690,7 +736,6 @@ def contains(self, other: Period) -> bool: For instance, ``period(2015)`` contains ``period(2015-01)``. """ - return self.start <= other.start and self.stop >= other.stop @property @@ -726,31 +771,29 @@ def stop(self) -> Instant: Instant((2012, 3, 1)) """ - unit, start_instant, size = self year, month, day = start_instant if unit == DateUnit.ETERNITY: return Instant((float("inf"), float("inf"), float("inf"))) - elif unit == DateUnit.YEAR: + if unit == DateUnit.YEAR: date = start_instant.date.add(years=size, days=-1) return Instant((date.year, date.month, date.day)) - elif unit == DateUnit.MONTH: + if unit == DateUnit.MONTH: date = start_instant.date.add(months=size, days=-1) return Instant((date.year, date.month, date.day)) - elif unit == DateUnit.WEEK: + if unit == DateUnit.WEEK: date = start_instant.date.add(weeks=size, days=-1) return Instant((date.year, date.month, date.day)) - elif unit in (DateUnit.DAY, DateUnit.WEEKDAY): + if unit in (DateUnit.DAY, DateUnit.WEEKDAY): date = start_instant.date.add(days=size - 1) return Instant((date.year, date.month, date.day)) - else: - raise ValueError + raise ValueError # Reference periods diff --git a/openfisca_core/periods/tests/helpers/test_helpers.py b/openfisca_core/periods/tests/helpers/test_helpers.py index bb409323d1..3cbf078a2e 100644 --- a/openfisca_core/periods/tests/helpers/test_helpers.py +++ b/openfisca_core/periods/tests/helpers/test_helpers.py @@ -7,49 +7,49 @@ @pytest.mark.parametrize( - "arg, expected", + ("arg", "expected"), [ - [None, None], - [Instant((1, 1, 1)), datetime.date(1, 1, 1)], - [Instant((4, 2, 29)), datetime.date(4, 2, 29)], - [(1, 1, 1), datetime.date(1, 1, 1)], + (None, None), + (Instant((1, 1, 1)), datetime.date(1, 1, 1)), + (Instant((4, 2, 29)), datetime.date(4, 2, 29)), + ((1, 1, 1), datetime.date(1, 1, 1)), ], ) -def test_instant_date(arg, expected): +def test_instant_date(arg, expected) -> None: assert periods.instant_date(arg) == expected @pytest.mark.parametrize( - "arg, error", + ("arg", "error"), [ - [Instant((-1, 1, 1)), ValueError], - [Instant((1, -1, 1)), ValueError], - [Instant((1, 13, -1)), ValueError], - [Instant((1, 1, -1)), ValueError], - [Instant((1, 1, 32)), ValueError], - [Instant((1, 2, 29)), ValueError], - [Instant(("1", 1, 1)), TypeError], - [(1,), TypeError], - [(1, 1), TypeError], + (Instant((-1, 1, 1)), ValueError), + (Instant((1, -1, 1)), ValueError), + (Instant((1, 13, -1)), ValueError), + (Instant((1, 1, -1)), ValueError), + (Instant((1, 1, 32)), ValueError), + (Instant((1, 2, 29)), ValueError), + (Instant(("1", 1, 1)), TypeError), + ((1,), TypeError), + ((1, 1), TypeError), ], ) -def test_instant_date_with_an_invalid_argument(arg, error): +def test_instant_date_with_an_invalid_argument(arg, error) -> None: with pytest.raises(error): periods.instant_date(arg) @pytest.mark.parametrize( - "arg, expected", + ("arg", "expected"), [ - [Period((DateUnit.WEEKDAY, Instant((1, 1, 1)), 5)), "100_5"], - [Period((DateUnit.WEEK, Instant((1, 1, 1)), 26)), "200_26"], - [Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), "100_365"], - [Period((DateUnit.MONTH, Instant((1, 1, 1)), 12)), "200_12"], - [Period((DateUnit.YEAR, Instant((1, 1, 1)), 2)), "300_2"], - [Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"], - [(DateUnit.DAY, None, 1), "100_1"], - [(DateUnit.MONTH, None, -1000), "200_-1000"], + (Period((DateUnit.WEEKDAY, Instant((1, 1, 1)), 5)), "100_5"), + (Period((DateUnit.WEEK, Instant((1, 1, 1)), 26)), "200_26"), + (Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), "100_365"), + (Period((DateUnit.MONTH, Instant((1, 1, 1)), 12)), "200_12"), + (Period((DateUnit.YEAR, Instant((1, 1, 1)), 2)), "300_2"), + (Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"), + ((DateUnit.DAY, None, 1), "100_1"), + ((DateUnit.MONTH, None, -1000), "200_-1000"), ], ) -def test_key_period_size(arg, expected): +def test_key_period_size(arg, expected) -> None: assert periods.key_period_size(arg) == expected diff --git a/openfisca_core/periods/tests/helpers/test_instant.py b/openfisca_core/periods/tests/helpers/test_instant.py index cb74c55ca4..73f37ece6f 100644 --- a/openfisca_core/periods/tests/helpers/test_instant.py +++ b/openfisca_core/periods/tests/helpers/test_instant.py @@ -7,70 +7,70 @@ @pytest.mark.parametrize( - "arg, expected", + ("arg", "expected"), [ - [None, None], - [datetime.date(1, 1, 1), Instant((1, 1, 1))], - [Instant((1, 1, 1)), Instant((1, 1, 1))], - [Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))], - [-1, Instant((-1, 1, 1))], - [0, Instant((0, 1, 1))], - [1, Instant((1, 1, 1))], - [999, Instant((999, 1, 1))], - [1000, Instant((1000, 1, 1))], - ["1000", Instant((1000, 1, 1))], - ["1000-01", Instant((1000, 1, 1))], - ["1000-01-01", Instant((1000, 1, 1))], - [(None,), Instant((None, 1, 1))], - [(None, None), Instant((None, None, 1))], - [(None, None, None), Instant((None, None, None))], - [(datetime.date(1, 1, 1),), Instant((datetime.date(1, 1, 1), 1, 1))], - [(Instant((1, 1, 1)),), Instant((Instant((1, 1, 1)), 1, 1))], - [ + (None, None), + (datetime.date(1, 1, 1), Instant((1, 1, 1))), + (Instant((1, 1, 1)), Instant((1, 1, 1))), + (Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))), + (-1, Instant((-1, 1, 1))), + (0, Instant((0, 1, 1))), + (1, Instant((1, 1, 1))), + (999, Instant((999, 1, 1))), + (1000, Instant((1000, 1, 1))), + ("1000", Instant((1000, 1, 1))), + ("1000-01", Instant((1000, 1, 1))), + ("1000-01-01", Instant((1000, 1, 1))), + ((None,), Instant((None, 1, 1))), + ((None, None), Instant((None, None, 1))), + ((None, None, None), Instant((None, None, None))), + ((datetime.date(1, 1, 1),), Instant((datetime.date(1, 1, 1), 1, 1))), + ((Instant((1, 1, 1)),), Instant((Instant((1, 1, 1)), 1, 1))), + ( (Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), Instant((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), 1, 1)), - ], - [(-1,), Instant((-1, 1, 1))], - [(-1, -1), Instant((-1, -1, 1))], - [(-1, -1, -1), Instant((-1, -1, -1))], - [("-1",), Instant(("-1", 1, 1))], - [("-1", "-1"), Instant(("-1", "-1", 1))], - [("-1", "-1", "-1"), Instant(("-1", "-1", "-1"))], - [("1-1",), Instant(("1-1", 1, 1))], - [("1-1-1",), Instant(("1-1-1", 1, 1))], + ), + ((-1,), Instant((-1, 1, 1))), + ((-1, -1), Instant((-1, -1, 1))), + ((-1, -1, -1), Instant((-1, -1, -1))), + (("-1",), Instant(("-1", 1, 1))), + (("-1", "-1"), Instant(("-1", "-1", 1))), + (("-1", "-1", "-1"), Instant(("-1", "-1", "-1"))), + (("1-1",), Instant(("1-1", 1, 1))), + (("1-1-1",), Instant(("1-1-1", 1, 1))), ], ) -def test_instant(arg, expected): +def test_instant(arg, expected) -> None: assert periods.instant(arg) == expected @pytest.mark.parametrize( - "arg, error", + ("arg", "error"), [ - [DateUnit.YEAR, ValueError], - [DateUnit.ETERNITY, ValueError], - ["1000-0", ValueError], - ["1000-0-0", ValueError], - ["1000-1", ValueError], - ["1000-1-1", ValueError], - ["1", ValueError], - ["a", ValueError], - ["year", ValueError], - ["eternity", ValueError], - ["999", ValueError], - ["1:1000-01-01", ValueError], - ["a:1000-01-01", ValueError], - ["year:1000-01-01", ValueError], - ["year:1000-01-01:1", ValueError], - ["year:1000-01-01:3", ValueError], - ["1000-01-01:a", ValueError], - ["1000-01-01:1", ValueError], - [(), AssertionError], - [{}, AssertionError], - ["", ValueError], - [(None, None, None, None), AssertionError], + (DateUnit.YEAR, ValueError), + (DateUnit.ETERNITY, ValueError), + ("1000-0", ValueError), + ("1000-0-0", ValueError), + ("1000-1", ValueError), + ("1000-1-1", ValueError), + ("1", ValueError), + ("a", ValueError), + ("year", ValueError), + ("eternity", ValueError), + ("999", ValueError), + ("1:1000-01-01", ValueError), + ("a:1000-01-01", ValueError), + ("year:1000-01-01", ValueError), + ("year:1000-01-01:1", ValueError), + ("year:1000-01-01:3", ValueError), + ("1000-01-01:a", ValueError), + ("1000-01-01:1", ValueError), + ((), AssertionError), + ({}, AssertionError), + ("", ValueError), + ((None, None, None, None), AssertionError), ], ) -def test_instant_with_an_invalid_argument(arg, error): +def test_instant_with_an_invalid_argument(arg, error) -> None: with pytest.raises(error): periods.instant(arg) diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py index 7d50abe102..c31e54c2ca 100644 --- a/openfisca_core/periods/tests/helpers/test_period.py +++ b/openfisca_core/periods/tests/helpers/test_period.py @@ -7,128 +7,128 @@ @pytest.mark.parametrize( - "arg, expected", + ("arg", "expected"), [ - ["eternity", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf")))], - ["ETERNITY", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf")))], - [ + ("eternity", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf")))), + ("ETERNITY", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf")))), + ( DateUnit.ETERNITY, Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf"))), - ], - [datetime.date(1, 1, 1), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))], - [Instant((1, 1, 1)), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))], - [ + ), + (datetime.date(1, 1, 1), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))), + (Instant((1, 1, 1)), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))), + ( Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), - ], - [-1, Period((DateUnit.YEAR, Instant((-1, 1, 1)), 1))], - [0, Period((DateUnit.YEAR, Instant((0, 1, 1)), 1))], - [1, Period((DateUnit.YEAR, Instant((1, 1, 1)), 1))], - [999, Period((DateUnit.YEAR, Instant((999, 1, 1)), 1))], - [1000, Period((DateUnit.YEAR, Instant((1000, 1, 1)), 1))], - ["1001", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["1001-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], - ["1001-01-01", Period((DateUnit.DAY, Instant((1001, 1, 1)), 1))], - ["1004-02-29", Period((DateUnit.DAY, Instant((1004, 2, 29)), 1))], - ["1001-W01", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))], - ["year:1001", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-01", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-01-01", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-W01", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))], - ["year:1001-W01-1", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))], - ["year:1001:1", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-01:1", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-01-01:1", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["year:1001-W01:1", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))], - ["year:1001-W01-1:1", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))], - ["year:1001:3", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3))], - ["year:1001-01:3", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3))], - ["year:1001-01-01:3", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3))], - ["year:1001-W01:3", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 3))], - ["year:1001-W01-1:3", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 3))], - ["month:1001-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], - ["month:1001-01-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], - ["week:1001-W01", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["week:1001-W01-1", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["month:1001-01:1", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], - ["month:1001-01:3", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 3))], - ["month:1001-01-01:3", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 3))], - ["week:1001-W01:1", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["week:1001-W01:3", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 3))], - ["week:1001-W01-1:3", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 3))], - ["day:1001-01-01", Period((DateUnit.DAY, Instant((1001, 1, 1)), 1))], - ["day:1001-01-01:3", Period((DateUnit.DAY, Instant((1001, 1, 1)), 3))], - ["weekday:1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))], - [ + ), + (-1, Period((DateUnit.YEAR, Instant((-1, 1, 1)), 1))), + (0, Period((DateUnit.YEAR, Instant((0, 1, 1)), 1))), + (1, Period((DateUnit.YEAR, Instant((1, 1, 1)), 1))), + (999, Period((DateUnit.YEAR, Instant((999, 1, 1)), 1))), + (1000, Period((DateUnit.YEAR, Instant((1000, 1, 1)), 1))), + ("1001", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))), + ("1001-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))), + ("1001-01-01", Period((DateUnit.DAY, Instant((1001, 1, 1)), 1))), + ("1004-02-29", Period((DateUnit.DAY, Instant((1004, 2, 29)), 1))), + ("1001-W01", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))), + ("1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))), + ("year:1001", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))), + ("year:1001-01", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))), + ("year:1001-01-01", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))), + ("year:1001-W01", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))), + ("year:1001-W01-1", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))), + ("year:1001:1", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))), + ("year:1001-01:1", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))), + ("year:1001-01-01:1", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))), + ("year:1001-W01:1", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))), + ("year:1001-W01-1:1", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 1))), + ("year:1001:3", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3))), + ("year:1001-01:3", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3))), + ("year:1001-01-01:3", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 3))), + ("year:1001-W01:3", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 3))), + ("year:1001-W01-1:3", Period((DateUnit.YEAR, Instant((1000, 12, 29)), 3))), + ("month:1001-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))), + ("month:1001-01-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))), + ("week:1001-W01", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))), + ("week:1001-W01-1", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))), + ("month:1001-01:1", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))), + ("month:1001-01:3", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 3))), + ("month:1001-01-01:3", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 3))), + ("week:1001-W01:1", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))), + ("week:1001-W01:3", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 3))), + ("week:1001-W01-1:3", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 3))), + ("day:1001-01-01", Period((DateUnit.DAY, Instant((1001, 1, 1)), 1))), + ("day:1001-01-01:3", Period((DateUnit.DAY, Instant((1001, 1, 1)), 3))), + ("weekday:1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))), + ( "weekday:1001-W01-1:3", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 3)), - ], + ), ], ) -def test_period(arg, expected): +def test_period(arg, expected) -> None: assert periods.period(arg) == expected @pytest.mark.parametrize( - "arg, error", + ("arg", "error"), [ - [None, ValueError], - [DateUnit.YEAR, ValueError], - ["1", ValueError], - ["999", ValueError], - ["1000-0", ValueError], - ["1000-13", ValueError], - ["1000-W0", ValueError], - ["1000-W54", ValueError], - ["1000-0-0", ValueError], - ["1000-1-0", ValueError], - ["1000-2-31", ValueError], - ["1000-W0-0", ValueError], - ["1000-W1-0", ValueError], - ["1000-W1-8", ValueError], - ["a", ValueError], - ["year", ValueError], - ["1:1000", ValueError], - ["a:1000", ValueError], - ["month:1000", ValueError], - ["week:1000", ValueError], - ["day:1000-01", ValueError], - ["weekday:1000-W1", ValueError], - ["1000:a", ValueError], - ["1000:1", ValueError], - ["1000-01:1", ValueError], - ["1000-01-01:1", ValueError], - ["1000-W1:1", ValueError], - ["1000-W1-1:1", ValueError], - ["month:1000:1", ValueError], - ["week:1000:1", ValueError], - ["day:1000:1", ValueError], - ["day:1000-01:1", ValueError], - ["weekday:1000:1", ValueError], - ["weekday:1000-W1:1", ValueError], - [(), ValueError], - [{}, ValueError], - ["", ValueError], - [(None,), ValueError], - [(None, None), ValueError], - [(None, None, None), ValueError], - [(None, None, None, None), ValueError], - [(Instant((1, 1, 1)),), ValueError], - [(Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), ValueError], - [(1,), ValueError], - [(1, 1), ValueError], - [(1, 1, 1), ValueError], - [(-1,), ValueError], - [(-1, -1), ValueError], - [(-1, -1, -1), ValueError], - [("-1",), ValueError], - [("-1", "-1"), ValueError], - [("-1", "-1", "-1"), ValueError], - [("1-1",), ValueError], - [("1-1-1",), ValueError], + (None, ValueError), + (DateUnit.YEAR, ValueError), + ("1", ValueError), + ("999", ValueError), + ("1000-0", ValueError), + ("1000-13", ValueError), + ("1000-W0", ValueError), + ("1000-W54", ValueError), + ("1000-0-0", ValueError), + ("1000-1-0", ValueError), + ("1000-2-31", ValueError), + ("1000-W0-0", ValueError), + ("1000-W1-0", ValueError), + ("1000-W1-8", ValueError), + ("a", ValueError), + ("year", ValueError), + ("1:1000", ValueError), + ("a:1000", ValueError), + ("month:1000", ValueError), + ("week:1000", ValueError), + ("day:1000-01", ValueError), + ("weekday:1000-W1", ValueError), + ("1000:a", ValueError), + ("1000:1", ValueError), + ("1000-01:1", ValueError), + ("1000-01-01:1", ValueError), + ("1000-W1:1", ValueError), + ("1000-W1-1:1", ValueError), + ("month:1000:1", ValueError), + ("week:1000:1", ValueError), + ("day:1000:1", ValueError), + ("day:1000-01:1", ValueError), + ("weekday:1000:1", ValueError), + ("weekday:1000-W1:1", ValueError), + ((), ValueError), + ({}, ValueError), + ("", ValueError), + ((None,), ValueError), + ((None, None), ValueError), + ((None, None, None), ValueError), + ((None, None, None, None), ValueError), + ((Instant((1, 1, 1)),), ValueError), + ((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), ValueError), + ((1,), ValueError), + ((1, 1), ValueError), + ((1, 1, 1), ValueError), + ((-1,), ValueError), + ((-1, -1), ValueError), + ((-1, -1, -1), ValueError), + (("-1",), ValueError), + (("-1", "-1"), ValueError), + (("-1", "-1", "-1"), ValueError), + (("1-1",), ValueError), + (("1-1-1",), ValueError), ], ) -def test_period_with_an_invalid_argument(arg, error): +def test_period_with_an_invalid_argument(arg, error) -> None: with pytest.raises(error): periods.period(arg) diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test__parsers.py index 6c88c9cd11..67a2891a32 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test__parsers.py @@ -5,65 +5,65 @@ @pytest.mark.parametrize( - "arg, expected", + ("arg", "expected"), [ - ["1001", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))], - ["1001-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))], - ["1001-12", Period((DateUnit.MONTH, Instant((1001, 12, 1)), 1))], - ["1001-01-01", Period((DateUnit.DAY, Instant((1001, 1, 1)), 1))], - ["1001-W01", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))], - ["1001-W52", Period((DateUnit.WEEK, Instant((1001, 12, 21)), 1))], - ["1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))], + ("1001", Period((DateUnit.YEAR, Instant((1001, 1, 1)), 1))), + ("1001-01", Period((DateUnit.MONTH, Instant((1001, 1, 1)), 1))), + ("1001-12", Period((DateUnit.MONTH, Instant((1001, 12, 1)), 1))), + ("1001-01-01", Period((DateUnit.DAY, Instant((1001, 1, 1)), 1))), + ("1001-W01", Period((DateUnit.WEEK, Instant((1000, 12, 29)), 1))), + ("1001-W52", Period((DateUnit.WEEK, Instant((1001, 12, 21)), 1))), + ("1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))), ], ) -def test__parse_period(arg, expected): +def test__parse_period(arg, expected) -> None: assert _parsers._parse_period(arg) == expected @pytest.mark.parametrize( - "arg, error", + ("arg", "error"), [ - [None, AttributeError], - [{}, AttributeError], - [(), AttributeError], - [[], AttributeError], - [1, AttributeError], - ["", AttributeError], - ["à", ParserError], - ["1", ValueError], - ["-1", ValueError], - ["999", ParserError], - ["1000-0", ParserError], - ["1000-1", ParserError], - ["1000-1-1", ParserError], - ["1000-00", ParserError], - ["1000-13", ParserError], - ["1000-01-00", ParserError], - ["1000-01-99", ParserError], - ["1000-W0", ParserError], - ["1000-W1", ParserError], - ["1000-W99", ParserError], - ["1000-W1-0", ParserError], - ["1000-W1-1", ParserError], - ["1000-W1-99", ParserError], - ["1000-W01-0", ParserError], - ["1000-W01-00", ParserError], + (None, AttributeError), + ({}, AttributeError), + ((), AttributeError), + ([], AttributeError), + (1, AttributeError), + ("", AttributeError), + ("à", ParserError), + ("1", ValueError), + ("-1", ValueError), + ("999", ParserError), + ("1000-0", ParserError), + ("1000-1", ParserError), + ("1000-1-1", ParserError), + ("1000-00", ParserError), + ("1000-13", ParserError), + ("1000-01-00", ParserError), + ("1000-01-99", ParserError), + ("1000-W0", ParserError), + ("1000-W1", ParserError), + ("1000-W99", ParserError), + ("1000-W1-0", ParserError), + ("1000-W1-1", ParserError), + ("1000-W1-99", ParserError), + ("1000-W01-0", ParserError), + ("1000-W01-00", ParserError), ], ) -def test__parse_period_with_invalid_argument(arg, error): +def test__parse_period_with_invalid_argument(arg, error) -> None: with pytest.raises(error): _parsers._parse_period(arg) @pytest.mark.parametrize( - "arg, expected", + ("arg", "expected"), [ - ["2022", DateUnit.YEAR], - ["2022-01", DateUnit.MONTH], - ["2022-01-01", DateUnit.DAY], - ["2022-W01", DateUnit.WEEK], - ["2022-W01-01", DateUnit.WEEKDAY], + ("2022", DateUnit.YEAR), + ("2022-01", DateUnit.MONTH), + ("2022-01-01", DateUnit.DAY), + ("2022-W01", DateUnit.WEEK), + ("2022-W01-01", DateUnit.WEEKDAY), ], ) -def test__parse_unit(arg, expected): +def test__parse_unit(arg, expected) -> None: assert _parsers._parse_unit(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 21549008f4..e9c73ef6aa 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -4,29 +4,29 @@ @pytest.mark.parametrize( - "instant, offset, unit, expected", + ("instant", "offset", "unit", "expected"), [ - [Instant((2020, 2, 29)), "first-of", DateUnit.YEAR, Instant((2020, 1, 1))], - [Instant((2020, 2, 29)), "first-of", DateUnit.MONTH, Instant((2020, 2, 1))], - [Instant((2020, 2, 29)), "first-of", DateUnit.WEEK, Instant((2020, 2, 24))], - [Instant((2020, 2, 29)), "first-of", DateUnit.DAY, None], - [Instant((2020, 2, 29)), "first-of", DateUnit.WEEKDAY, None], - [Instant((2020, 2, 29)), "last-of", DateUnit.YEAR, Instant((2020, 12, 31))], - [Instant((2020, 2, 29)), "last-of", DateUnit.MONTH, Instant((2020, 2, 29))], - [Instant((2020, 2, 29)), "last-of", DateUnit.WEEK, Instant((2020, 3, 1))], - [Instant((2020, 2, 29)), "last-of", DateUnit.DAY, None], - [Instant((2020, 2, 29)), "last-of", DateUnit.WEEKDAY, None], - [Instant((2020, 2, 29)), -3, DateUnit.YEAR, Instant((2017, 2, 28))], - [Instant((2020, 2, 29)), -3, DateUnit.MONTH, Instant((2019, 11, 29))], - [Instant((2020, 2, 29)), -3, DateUnit.WEEK, Instant((2020, 2, 8))], - [Instant((2020, 2, 29)), -3, DateUnit.DAY, Instant((2020, 2, 26))], - [Instant((2020, 2, 29)), -3, DateUnit.WEEKDAY, Instant((2020, 2, 26))], - [Instant((2020, 2, 29)), 3, DateUnit.YEAR, Instant((2023, 2, 28))], - [Instant((2020, 2, 29)), 3, DateUnit.MONTH, Instant((2020, 5, 29))], - [Instant((2020, 2, 29)), 3, DateUnit.WEEK, Instant((2020, 3, 21))], - [Instant((2020, 2, 29)), 3, DateUnit.DAY, Instant((2020, 3, 3))], - [Instant((2020, 2, 29)), 3, DateUnit.WEEKDAY, Instant((2020, 3, 3))], + (Instant((2020, 2, 29)), "first-of", DateUnit.YEAR, Instant((2020, 1, 1))), + (Instant((2020, 2, 29)), "first-of", DateUnit.MONTH, Instant((2020, 2, 1))), + (Instant((2020, 2, 29)), "first-of", DateUnit.WEEK, Instant((2020, 2, 24))), + (Instant((2020, 2, 29)), "first-of", DateUnit.DAY, None), + (Instant((2020, 2, 29)), "first-of", DateUnit.WEEKDAY, None), + (Instant((2020, 2, 29)), "last-of", DateUnit.YEAR, Instant((2020, 12, 31))), + (Instant((2020, 2, 29)), "last-of", DateUnit.MONTH, Instant((2020, 2, 29))), + (Instant((2020, 2, 29)), "last-of", DateUnit.WEEK, Instant((2020, 3, 1))), + (Instant((2020, 2, 29)), "last-of", DateUnit.DAY, None), + (Instant((2020, 2, 29)), "last-of", DateUnit.WEEKDAY, None), + (Instant((2020, 2, 29)), -3, DateUnit.YEAR, Instant((2017, 2, 28))), + (Instant((2020, 2, 29)), -3, DateUnit.MONTH, Instant((2019, 11, 29))), + (Instant((2020, 2, 29)), -3, DateUnit.WEEK, Instant((2020, 2, 8))), + (Instant((2020, 2, 29)), -3, DateUnit.DAY, Instant((2020, 2, 26))), + (Instant((2020, 2, 29)), -3, DateUnit.WEEKDAY, Instant((2020, 2, 26))), + (Instant((2020, 2, 29)), 3, DateUnit.YEAR, Instant((2023, 2, 28))), + (Instant((2020, 2, 29)), 3, DateUnit.MONTH, Instant((2020, 5, 29))), + (Instant((2020, 2, 29)), 3, DateUnit.WEEK, Instant((2020, 3, 21))), + (Instant((2020, 2, 29)), 3, DateUnit.DAY, Instant((2020, 3, 3))), + (Instant((2020, 2, 29)), 3, DateUnit.WEEKDAY, Instant((2020, 3, 3))), ], ) -def test_offset(instant, offset, unit, expected): +def test_offset(instant, offset, unit, expected) -> None: assert instant.offset(offset, unit) == expected diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 6553c4fd9b..9e53bf7d12 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -4,278 +4,278 @@ @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.YEAR, Instant((2022, 1, 1)), 1, "2022"], - [DateUnit.MONTH, Instant((2022, 1, 1)), 12, "2022"], - [DateUnit.YEAR, Instant((2022, 3, 1)), 1, "year:2022-03"], - [DateUnit.MONTH, Instant((2022, 3, 1)), 12, "year:2022-03"], - [DateUnit.YEAR, Instant((2022, 1, 1)), 3, "year:2022:3"], - [DateUnit.YEAR, Instant((2022, 1, 3)), 3, "year:2022:3"], + (DateUnit.YEAR, Instant((2022, 1, 1)), 1, "2022"), + (DateUnit.MONTH, Instant((2022, 1, 1)), 12, "2022"), + (DateUnit.YEAR, Instant((2022, 3, 1)), 1, "year:2022-03"), + (DateUnit.MONTH, Instant((2022, 3, 1)), 12, "year:2022-03"), + (DateUnit.YEAR, Instant((2022, 1, 1)), 3, "year:2022:3"), + (DateUnit.YEAR, Instant((2022, 1, 3)), 3, "year:2022:3"), ], ) -def test_str_with_years(date_unit, instant, size, expected): +def test_str_with_years(date_unit, instant, size, expected) -> None: assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.MONTH, Instant((2022, 1, 1)), 1, "2022-01"], - [DateUnit.MONTH, Instant((2022, 1, 1)), 3, "month:2022-01:3"], - [DateUnit.MONTH, Instant((2022, 3, 1)), 3, "month:2022-03:3"], + (DateUnit.MONTH, Instant((2022, 1, 1)), 1, "2022-01"), + (DateUnit.MONTH, Instant((2022, 1, 1)), 3, "month:2022-01:3"), + (DateUnit.MONTH, Instant((2022, 3, 1)), 3, "month:2022-03:3"), ], ) -def test_str_with_months(date_unit, instant, size, expected): +def test_str_with_months(date_unit, instant, size, expected) -> None: assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.DAY, Instant((2022, 1, 1)), 1, "2022-01-01"], - [DateUnit.DAY, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], - [DateUnit.DAY, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], + (DateUnit.DAY, Instant((2022, 1, 1)), 1, "2022-01-01"), + (DateUnit.DAY, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"), + (DateUnit.DAY, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"), ], ) -def test_str_with_days(date_unit, instant, size, expected): +def test_str_with_days(date_unit, instant, size, expected) -> None: assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.WEEK, Instant((2022, 1, 1)), 1, "2021-W52"], - [DateUnit.WEEK, Instant((2022, 1, 1)), 3, "week:2021-W52:3"], - [DateUnit.WEEK, Instant((2022, 3, 1)), 1, "2022-W09"], - [DateUnit.WEEK, Instant((2022, 3, 1)), 3, "week:2022-W09:3"], + (DateUnit.WEEK, Instant((2022, 1, 1)), 1, "2021-W52"), + (DateUnit.WEEK, Instant((2022, 1, 1)), 3, "week:2021-W52:3"), + (DateUnit.WEEK, Instant((2022, 3, 1)), 1, "2022-W09"), + (DateUnit.WEEK, Instant((2022, 3, 1)), 3, "week:2022-W09:3"), ], ) -def test_str_with_weeks(date_unit, instant, size, expected): +def test_str_with_weeks(date_unit, instant, size, expected) -> None: assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.WEEKDAY, Instant((2022, 1, 1)), 1, "2021-W52-6"], - [DateUnit.WEEKDAY, Instant((2022, 1, 1)), 3, "weekday:2021-W52-6:3"], - [DateUnit.WEEKDAY, Instant((2022, 3, 1)), 1, "2022-W09-2"], - [DateUnit.WEEKDAY, Instant((2022, 3, 1)), 3, "weekday:2022-W09-2:3"], + (DateUnit.WEEKDAY, Instant((2022, 1, 1)), 1, "2021-W52-6"), + (DateUnit.WEEKDAY, Instant((2022, 1, 1)), 3, "weekday:2021-W52-6:3"), + (DateUnit.WEEKDAY, Instant((2022, 3, 1)), 1, "2022-W09-2"), + (DateUnit.WEEKDAY, Instant((2022, 3, 1)), 3, "weekday:2022-W09-2:3"), ], ) -def test_str_with_weekdays(date_unit, instant, size, expected): +def test_str_with_weekdays(date_unit, instant, size, expected) -> None: assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.YEAR, Instant((2022, 12, 1)), 1, 1], - [DateUnit.YEAR, Instant((2022, 1, 1)), 2, 2], + (DateUnit.YEAR, Instant((2022, 12, 1)), 1, 1), + (DateUnit.YEAR, Instant((2022, 1, 1)), 2, 2), ], ) -def test_size_in_years(date_unit, instant, size, expected): +def test_size_in_years(date_unit, instant, size, expected) -> None: period = Period((date_unit, instant, size)) assert period.size_in_years == expected @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.YEAR, Instant((2020, 1, 1)), 1, 12], - [DateUnit.YEAR, Instant((2022, 1, 1)), 2, 24], - [DateUnit.MONTH, Instant((2012, 1, 3)), 3, 3], + (DateUnit.YEAR, Instant((2020, 1, 1)), 1, 12), + (DateUnit.YEAR, Instant((2022, 1, 1)), 2, 24), + (DateUnit.MONTH, Instant((2012, 1, 3)), 3, 3), ], ) -def test_size_in_months(date_unit, instant, size, expected): +def test_size_in_months(date_unit, instant, size, expected) -> None: period = Period((date_unit, instant, size)) assert period.size_in_months == expected @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.YEAR, Instant((2022, 12, 1)), 1, 365], - [DateUnit.YEAR, Instant((2020, 1, 1)), 1, 366], - [DateUnit.YEAR, Instant((2022, 1, 1)), 2, 730], - [DateUnit.MONTH, Instant((2022, 12, 1)), 1, 31], - [DateUnit.MONTH, Instant((2020, 2, 3)), 1, 29], - [DateUnit.MONTH, Instant((2022, 1, 3)), 3, 31 + 28 + 31], - [DateUnit.MONTH, Instant((2012, 1, 3)), 3, 31 + 29 + 31], - [DateUnit.DAY, Instant((2022, 12, 31)), 1, 1], - [DateUnit.DAY, Instant((2022, 12, 31)), 3, 3], - [DateUnit.WEEK, Instant((2022, 12, 31)), 1, 7], - [DateUnit.WEEK, Instant((2022, 12, 31)), 3, 21], - [DateUnit.WEEKDAY, Instant((2022, 12, 31)), 1, 1], - [DateUnit.WEEKDAY, Instant((2022, 12, 31)), 3, 3], + (DateUnit.YEAR, Instant((2022, 12, 1)), 1, 365), + (DateUnit.YEAR, Instant((2020, 1, 1)), 1, 366), + (DateUnit.YEAR, Instant((2022, 1, 1)), 2, 730), + (DateUnit.MONTH, Instant((2022, 12, 1)), 1, 31), + (DateUnit.MONTH, Instant((2020, 2, 3)), 1, 29), + (DateUnit.MONTH, Instant((2022, 1, 3)), 3, 31 + 28 + 31), + (DateUnit.MONTH, Instant((2012, 1, 3)), 3, 31 + 29 + 31), + (DateUnit.DAY, Instant((2022, 12, 31)), 1, 1), + (DateUnit.DAY, Instant((2022, 12, 31)), 3, 3), + (DateUnit.WEEK, Instant((2022, 12, 31)), 1, 7), + (DateUnit.WEEK, Instant((2022, 12, 31)), 3, 21), + (DateUnit.WEEKDAY, Instant((2022, 12, 31)), 1, 1), + (DateUnit.WEEKDAY, Instant((2022, 12, 31)), 3, 3), ], ) -def test_size_in_days(date_unit, instant, size, expected): +def test_size_in_days(date_unit, instant, size, expected) -> None: period = Period((date_unit, instant, size)) assert period.size_in_days == expected assert period.size_in_days == period.days @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.YEAR, Instant((2022, 12, 1)), 1, 52], - [DateUnit.YEAR, Instant((2020, 1, 1)), 5, 261], - [DateUnit.MONTH, Instant((2022, 12, 1)), 1, 4], - [DateUnit.MONTH, Instant((2020, 2, 3)), 1, 4], - [DateUnit.MONTH, Instant((2022, 1, 3)), 3, 12], - [DateUnit.MONTH, Instant((2012, 1, 3)), 3, 13], - [DateUnit.WEEK, Instant((2022, 12, 31)), 1, 1], - [DateUnit.WEEK, Instant((2022, 12, 31)), 3, 3], + (DateUnit.YEAR, Instant((2022, 12, 1)), 1, 52), + (DateUnit.YEAR, Instant((2020, 1, 1)), 5, 261), + (DateUnit.MONTH, Instant((2022, 12, 1)), 1, 4), + (DateUnit.MONTH, Instant((2020, 2, 3)), 1, 4), + (DateUnit.MONTH, Instant((2022, 1, 3)), 3, 12), + (DateUnit.MONTH, Instant((2012, 1, 3)), 3, 13), + (DateUnit.WEEK, Instant((2022, 12, 31)), 1, 1), + (DateUnit.WEEK, Instant((2022, 12, 31)), 3, 3), ], ) -def test_size_in_weeks(date_unit, instant, size, expected): +def test_size_in_weeks(date_unit, instant, size, expected) -> None: period = Period((date_unit, instant, size)) assert period.size_in_weeks == expected @pytest.mark.parametrize( - "date_unit, instant, size, expected", + ("date_unit", "instant", "size", "expected"), [ - [DateUnit.YEAR, Instant((2022, 12, 1)), 1, 364], - [DateUnit.YEAR, Instant((2020, 1, 1)), 1, 364], - [DateUnit.YEAR, Instant((2022, 1, 1)), 2, 728], - [DateUnit.MONTH, Instant((2022, 12, 1)), 1, 31], - [DateUnit.MONTH, Instant((2020, 2, 3)), 1, 29], - [DateUnit.MONTH, Instant((2022, 1, 3)), 3, 31 + 28 + 31], - [DateUnit.MONTH, Instant((2012, 1, 3)), 3, 31 + 29 + 31], - [DateUnit.DAY, Instant((2022, 12, 31)), 1, 1], - [DateUnit.DAY, Instant((2022, 12, 31)), 3, 3], - [DateUnit.WEEK, Instant((2022, 12, 31)), 1, 7], - [DateUnit.WEEK, Instant((2022, 12, 31)), 3, 21], - [DateUnit.WEEKDAY, Instant((2022, 12, 31)), 1, 1], - [DateUnit.WEEKDAY, Instant((2022, 12, 31)), 3, 3], + (DateUnit.YEAR, Instant((2022, 12, 1)), 1, 364), + (DateUnit.YEAR, Instant((2020, 1, 1)), 1, 364), + (DateUnit.YEAR, Instant((2022, 1, 1)), 2, 728), + (DateUnit.MONTH, Instant((2022, 12, 1)), 1, 31), + (DateUnit.MONTH, Instant((2020, 2, 3)), 1, 29), + (DateUnit.MONTH, Instant((2022, 1, 3)), 3, 31 + 28 + 31), + (DateUnit.MONTH, Instant((2012, 1, 3)), 3, 31 + 29 + 31), + (DateUnit.DAY, Instant((2022, 12, 31)), 1, 1), + (DateUnit.DAY, Instant((2022, 12, 31)), 3, 3), + (DateUnit.WEEK, Instant((2022, 12, 31)), 1, 7), + (DateUnit.WEEK, Instant((2022, 12, 31)), 3, 21), + (DateUnit.WEEKDAY, Instant((2022, 12, 31)), 1, 1), + (DateUnit.WEEKDAY, Instant((2022, 12, 31)), 3, 3), ], ) -def test_size_in_weekdays(date_unit, instant, size, expected): +def test_size_in_weekdays(date_unit, instant, size, expected) -> None: period = Period((date_unit, instant, size)) assert period.size_in_weekdays == expected @pytest.mark.parametrize( - "period_unit, sub_unit, instant, start, cease, count", + ("period_unit", "sub_unit", "instant", "start", "cease", "count"), [ - [ + ( DateUnit.YEAR, DateUnit.YEAR, Instant((2022, 12, 31)), Instant((2022, 1, 1)), Instant((2024, 1, 1)), 3, - ], - [ + ), + ( DateUnit.YEAR, DateUnit.MONTH, Instant((2022, 12, 31)), Instant((2022, 12, 1)), Instant((2025, 11, 1)), 36, - ], - [ + ), + ( DateUnit.YEAR, DateUnit.DAY, Instant((2022, 12, 31)), Instant((2022, 12, 31)), Instant((2025, 12, 30)), 1096, - ], - [ + ), + ( DateUnit.YEAR, DateUnit.WEEK, Instant((2022, 12, 31)), Instant((2022, 12, 26)), Instant((2025, 12, 15)), 156, - ], - [ + ), + ( DateUnit.YEAR, DateUnit.WEEKDAY, Instant((2022, 12, 31)), Instant((2022, 12, 31)), Instant((2025, 12, 26)), 1092, - ], - [ + ), + ( DateUnit.MONTH, DateUnit.MONTH, Instant((2022, 12, 31)), Instant((2022, 12, 1)), Instant((2023, 2, 1)), 3, - ], - [ + ), + ( DateUnit.MONTH, DateUnit.DAY, Instant((2022, 12, 31)), Instant((2022, 12, 31)), Instant((2023, 3, 30)), 90, - ], - [ + ), + ( DateUnit.DAY, DateUnit.DAY, Instant((2022, 12, 31)), Instant((2022, 12, 31)), Instant((2023, 1, 2)), 3, - ], - [ + ), + ( DateUnit.DAY, DateUnit.WEEKDAY, Instant((2022, 12, 31)), Instant((2022, 12, 31)), Instant((2023, 1, 2)), 3, - ], - [ + ), + ( DateUnit.WEEK, DateUnit.DAY, Instant((2022, 12, 31)), Instant((2022, 12, 31)), Instant((2023, 1, 20)), 21, - ], - [ + ), + ( DateUnit.WEEK, DateUnit.WEEK, Instant((2022, 12, 31)), Instant((2022, 12, 26)), Instant((2023, 1, 9)), 3, - ], - [ + ), + ( DateUnit.WEEK, DateUnit.WEEKDAY, Instant((2022, 12, 31)), Instant((2022, 12, 31)), Instant((2023, 1, 20)), 21, - ], - [ + ), + ( DateUnit.WEEKDAY, DateUnit.DAY, Instant((2022, 12, 31)), Instant((2022, 12, 31)), Instant((2023, 1, 2)), 3, - ], - [ + ), + ( DateUnit.WEEKDAY, DateUnit.WEEKDAY, Instant((2022, 12, 31)), Instant((2022, 12, 31)), Instant((2023, 1, 2)), 3, - ], + ), ], ) -def test_subperiods(period_unit, sub_unit, instant, start, cease, count): +def test_subperiods(period_unit, sub_unit, instant, start, cease, count) -> None: period = Period((period_unit, instant, 3)) subperiods = period.get_subperiods(sub_unit) assert len(subperiods) == count diff --git a/openfisca_core/populations/group_population.py b/openfisca_core/populations/group_population.py index d77816face..4e68762f19 100644 --- a/openfisca_core/populations/group_population.py +++ b/openfisca_core/populations/group_population.py @@ -8,7 +8,7 @@ class GroupPopulation(Population): - def __init__(self, entity, members): + def __init__(self, entity, members) -> None: super().__init__(entity) self.members = members self._members_entity_id = None @@ -46,7 +46,7 @@ def members_position(self): return self._members_position @members_position.setter - def members_position(self, members_position): + def members_position(self, members_position) -> None: self._members_position = members_position @property @@ -54,7 +54,7 @@ def members_entity_id(self): return self._members_entity_id @members_entity_id.setter - def members_entity_id(self, members_entity_id): + def members_entity_id(self, members_entity_id) -> None: self._members_entity_id = members_entity_id @property @@ -65,14 +65,13 @@ def members_role(self): return self._members_role @members_role.setter - def members_role(self, members_role: typing.Iterable[entities.Role]): + def members_role(self, members_role: typing.Iterable[entities.Role]) -> None: if members_role is not None: self._members_role = numpy.array(list(members_role)) @property def ordered_members_map(self): - """ - Mask to group the persons by entity + """Mask to group the persons by entity This function only caches the map value, to see what the map is used for, see value_nth_person method. """ if self._ordered_members_map is None: @@ -89,18 +88,19 @@ def get_role(self, role_name): @projectors.projectable def sum(self, array, role=None): - """ - Return the sum of ``array`` for the members of the entity. + """Return the sum of ``array`` for the members of the entity. ``array`` must have the dimension of the number of persons in the simulation If ``role`` is provided, only the entity member with the given role are taken into account. Example: - - >>> salaries = household.members('salary', '2018-01') # e.g. [2000, 1500, 0, 0, 0] + >>> salaries = household.members( + ... "salary", "2018-01" + ... ) # e.g. [2000, 1500, 0, 0, 0] >>> household.sum(salaries) >>> array([3500]) + """ self.entity.check_role_validity(role) self.members.check_array_compatible_with_entity(array) @@ -111,23 +111,23 @@ def sum(self, array, role=None): weights=array[role_filter], minlength=self.count, ) - else: - return numpy.bincount(self.members_entity_id, weights=array) + return numpy.bincount(self.members_entity_id, weights=array) @projectors.projectable def any(self, array, role=None): - """ - Return ``True`` if ``array`` is ``True`` for any members of the entity. + """Return ``True`` if ``array`` is ``True`` for any members of the entity. ``array`` must have the dimension of the number of persons in the simulation If ``role`` is provided, only the entity member with the given role are taken into account. Example: - - >>> salaries = household.members('salary', '2018-01') # e.g. [2000, 1500, 0, 0, 0] + >>> salaries = household.members( + ... "salary", "2018-01" + ... ) # e.g. [2000, 1500, 0, 0, 0] >>> household.any(salaries >= 1800) >>> array([True]) + """ sum_in_entity = self.sum(array, role=role) return sum_in_entity > 0 @@ -141,7 +141,7 @@ def reduce(self, array, reducer, neutral_element, role=None): filtered_array = numpy.where(role_filter, array, neutral_element) result = self.filled_array( - neutral_element + neutral_element, ) # Neutral value that will be returned if no one with the given role exists. # We loop over the positions in the entity @@ -156,87 +156,98 @@ def reduce(self, array, reducer, neutral_element, role=None): @projectors.projectable def all(self, array, role=None): - """ - Return ``True`` if ``array`` is ``True`` for all members of the entity. + """Return ``True`` if ``array`` is ``True`` for all members of the entity. ``array`` must have the dimension of the number of persons in the simulation If ``role`` is provided, only the entity member with the given role are taken into account. Example: - - >>> salaries = household.members('salary', '2018-01') # e.g. [2000, 1500, 0, 0, 0] + >>> salaries = household.members( + ... "salary", "2018-01" + ... ) # e.g. [2000, 1500, 0, 0, 0] >>> household.all(salaries >= 1800) >>> array([False]) + """ return self.reduce( - array, reducer=numpy.logical_and, neutral_element=True, role=role + array, + reducer=numpy.logical_and, + neutral_element=True, + role=role, ) @projectors.projectable def max(self, array, role=None): - """ - Return the maximum value of ``array`` for the entity members. + """Return the maximum value of ``array`` for the entity members. ``array`` must have the dimension of the number of persons in the simulation If ``role`` is provided, only the entity member with the given role are taken into account. Example: - - >>> salaries = household.members('salary', '2018-01') # e.g. [2000, 1500, 0, 0, 0] + >>> salaries = household.members( + ... "salary", "2018-01" + ... ) # e.g. [2000, 1500, 0, 0, 0] >>> household.max(salaries) >>> array([2000]) + """ return self.reduce( - array, reducer=numpy.maximum, neutral_element=-numpy.infty, role=role + array, + reducer=numpy.maximum, + neutral_element=-numpy.inf, + role=role, ) @projectors.projectable def min(self, array, role=None): - """ - Return the minimum value of ``array`` for the entity members. + """Return the minimum value of ``array`` for the entity members. ``array`` must have the dimension of the number of persons in the simulation If ``role`` is provided, only the entity member with the given role are taken into account. Example: - - >>> salaries = household.members('salary', '2018-01') # e.g. [2000, 1500, 0, 0, 0] + >>> salaries = household.members( + ... "salary", "2018-01" + ... ) # e.g. [2000, 1500, 0, 0, 0] >>> household.min(salaries) >>> array([0]) - >>> household.min(salaries, role = Household.PARENT) # Assuming the 1st two persons are parents + >>> household.min( + ... salaries, role=Household.PARENT + ... ) # Assuming the 1st two persons are parents >>> array([1500]) + """ return self.reduce( - array, reducer=numpy.minimum, neutral_element=numpy.infty, role=role + array, + reducer=numpy.minimum, + neutral_element=numpy.inf, + role=role, ) @projectors.projectable def nb_persons(self, role=None): - """ - Returns the number of persons contained in the entity. + """Returns the number of persons contained in the entity. If ``role`` is provided, only the entity member with the given role are taken into account. """ if role: if role.subroles: role_condition = numpy.logical_or.reduce( - [self.members_role == subrole for subrole in role.subroles] + [self.members_role == subrole for subrole in role.subroles], ) else: role_condition = self.members_role == role return self.sum(role_condition) - else: - return numpy.bincount(self.members_entity_id) + return numpy.bincount(self.members_entity_id) # Projection person -> entity @projectors.projectable def value_from_person(self, array, role, default=0): - """ - Get the value of ``array`` for the person with the unique role ``role``. + """Get the value of ``array`` for the person with the unique role ``role``. ``array`` must have the dimension of the number of persons in the simulation @@ -246,10 +257,9 @@ def value_from_person(self, array, role, default=0): """ self.entity.check_role_validity(role) if role.max != 1: + msg = f"You can only use value_from_person with a role that is unique in {self.key}. Role {role.key} is not unique." raise Exception( - "You can only use value_from_person with a role that is unique in {}. Role {} is not unique.".format( - self.key, role.key - ) + msg, ) self.members.check_array_compatible_with_entity(array) members_map = self.ordered_members_map @@ -265,8 +275,7 @@ def value_from_person(self, array, role, default=0): @projectors.projectable def value_nth_person(self, n, array, default=0): - """ - Get the value of array for the person whose position in the entity is n. + """Get the value of array for the person whose position in the entity is n. Note that this position is arbitrary, and that members are not sorted. @@ -301,6 +310,5 @@ def project(self, array, role=None): self.entity.check_role_validity(role) if role is None: return array[self.members_entity_id] - else: - role_condition = self.members.has_role(role) - return numpy.where(role_condition, array[self.members_entity_id], 0) + role_condition = self.members.has_role(role) + return numpy.where(role_condition, array[self.members_entity_id], 0) diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index e3ef6b209a..f9eee1c2a2 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import Dict, NamedTuple, Optional, Sequence, Union +from collections.abc import Sequence +from typing import NamedTuple from typing_extensions import TypedDict from openfisca_core.types import Array, Period, Role, Simulation, SingleEntity @@ -15,9 +16,9 @@ class Population: - simulation: Optional[Simulation] + simulation: Simulation | None entity: SingleEntity - _holders: Dict[str, holders.Holder] + _holders: dict[str, holders.Holder] count: int ids: Array[str] @@ -44,22 +45,21 @@ def empty_array(self) -> Array[float]: def filled_array( self, - value: Union[float, bool], - dtype: Optional[numpy.dtype] = None, - ) -> Union[Array[float], Array[bool]]: + value: float | bool, + dtype: numpy.dtype | None = None, + ) -> Array[float] | Array[bool]: return numpy.full(self.count, value, dtype) def __getattr__(self, attribute: str) -> projectors.Projector: - projector: Optional[projectors.Projector] + projector: projectors.Projector | None projector = projectors.get_projector_from_shortcut(self, attribute) if isinstance(projector, projectors.Projector): return projector + msg = f"You tried to use the '{attribute}' of '{self.entity.key}' but that is not a known attribute." raise AttributeError( - "You tried to use the '{}' of '{}' but that is not a known attribute.".format( - attribute, self.entity.key - ) + msg, ) def get_index(self, id: str) -> int: @@ -72,51 +72,48 @@ def check_array_compatible_with_entity( array: Array[float], ) -> None: if self.count == array.size: - return None + return + msg = f"Input {array} is not a valid value for the entity {self.entity.key} (size = {array.size} != {self.count} = count)" raise ValueError( - "Input {} is not a valid value for the entity {} (size = {} != {} = count)".format( - array, self.entity.key, array.size, self.count - ) + msg, ) def check_period_validity( self, variable_name: str, - period: Optional[Union[int, str, Period]], + period: int | str | Period | None, ) -> None: if isinstance(period, (int, str, Period)): - return None + return stack = traceback.extract_stack() filename, line_number, function_name, line_of_code = stack[-3] - raise ValueError( - """ -You requested computation of variable "{}", but you did not specify on which period in "{}:{}": - {} + msg = f""" +You requested computation of variable "{variable_name}", but you did not specify on which period in "{filename}:{line_number}": + {line_of_code} When you request the computation of a variable within a formula, you must always specify the period as the second parameter. The convention is to call this parameter "period". For example: computed_salary = person('salary', period). See more information at . -""".format( - variable_name, filename, line_number, line_of_code - ) +""" + raise ValueError( + msg, ) def __call__( self, variable_name: str, - period: Optional[Union[int, str, Period]] = None, - options: Optional[Sequence[str]] = None, - ) -> Optional[Array[float]]: - """ - Calculate the variable ``variable_name`` for the entity and the period ``period``, using the variable formula if it exists. + period: int | str | Period | None = None, + options: Sequence[str] | None = None, + ) -> Array[float] | None: + """Calculate the variable ``variable_name`` for the entity and the period ``period``, using the variable formula if it exists. Example: - - >>> person('salary', '2017-04') - >>> array([300.]) + >>> person("salary", "2017-04") + >>> array([300.0]) :returns: A numpy array containing the result of the calculation + """ if self.simulation is None: return None @@ -149,11 +146,7 @@ def __call__( ) raise ValueError( - "Options config.ADD and config.DIVIDE are incompatible (trying to compute variable {})".format( - variable_name - ).encode( - "utf-8" - ) + f"Options config.ADD and config.DIVIDE are incompatible (trying to compute variable {variable_name})".encode(), ) # Helpers @@ -169,7 +162,7 @@ def get_holder(self, variable_name: str) -> holders.Holder: def get_memory_usage( self, - variables: Optional[Sequence[str]] = None, + variables: Sequence[str] | None = None, ) -> MemoryUsageByVariable: holders_memory_usage = { variable_name: holder.get_memory_usage() @@ -186,20 +179,18 @@ def get_memory_usage( { "total_nb_bytes": total_memory_usage, "by_variable": holders_memory_usage, - } + }, ) @projectors.projectable - def has_role(self, role: Role) -> Optional[Array[bool]]: - """ - Check if a person has a given role within its `GroupEntity` + def has_role(self, role: Role) -> Array[bool] | None: + """Check if a person has a given role within its `GroupEntity`. Example: - >>> person.has_role(Household.CHILD) >>> array([False]) - """ + """ if self.simulation is None: return None @@ -209,11 +200,10 @@ def has_role(self, role: Role) -> Optional[Array[bool]]: if role.subroles: return numpy.logical_or.reduce( - [group_population.members_role == subrole for subrole in role.subroles] + [group_population.members_role == subrole for subrole in role.subroles], ) - else: - return group_population.members_role == role + return group_population.members_role == role @projectors.projectable def value_from_partner( @@ -221,13 +211,14 @@ def value_from_partner( array: Array[float], entity: projectors.Projector, role: Role, - ) -> Optional[Array[float]]: + ) -> Array[float] | None: self.check_array_compatible_with_entity(array) self.entity.check_role_validity(role) - if not role.subroles or not len(role.subroles) == 2: + if not role.subroles or len(role.subroles) != 2: + msg = "Projection to partner is only implemented for roles having exactly two subroles." raise Exception( - "Projection to partner is only implemented for roles having exactly two subroles." + msg, ) [subrole_1, subrole_2] = role.subroles @@ -246,22 +237,24 @@ def get_rank( criteria: Array[float], condition: bool = True, ) -> Array[int]: - """ - Get the rank of a person within an entity according to a criteria. + """Get the rank of a person within an entity according to a criteria. The person with rank 0 has the minimum value of criteria. If condition is specified, then the persons who don't respect it are not taken into account and their rank is -1. Example: - - >>> age = person('age', period) # e.g [32, 34, 2, 8, 1] + >>> age = person("age", period) # e.g [32, 34, 2, 8, 1] >>> person.get_rank(household, age) >>> [3, 4, 0, 2, 1] - >>> is_child = person.has_role(Household.CHILD) # [False, False, True, True, True] - >>> person.get_rank(household, - age, condition = is_child) # Sort in reverse order so that the eldest child gets the rank 0. + >>> is_child = person.has_role( + ... Household.CHILD + ... ) # [False, False, True, True, True] + >>> person.get_rank( + ... household, -age, condition=is_child + ... ) # Sort in reverse order so that the eldest child gets the rank 0. >>> [-1, -1, 1, 0, 2] - """ + """ # If entity is for instance 'person.household', we get the reference entity 'household' behind the projector entity = ( entity @@ -279,7 +272,7 @@ def get_rank( [ entity.value_nth_person(k, filtered_criteria, default=numpy.inf) for k in range(biggest_entity_size) - ] + ], ).transpose() # We double-argsort all lines of the matrix. @@ -297,9 +290,9 @@ def get_rank( class Calculate(NamedTuple): variable: str period: Period - option: Optional[Sequence[str]] + option: Sequence[str] | None class MemoryUsageByVariable(TypedDict, total=False): - by_variable: Dict[str, holders.MemoryUsage] + by_variable: dict[str, holders.MemoryUsage] total_nb_bytes: int diff --git a/openfisca_core/projectors/entity_to_person_projector.py b/openfisca_core/projectors/entity_to_person_projector.py index ca6245a1f7..392fda08a1 100644 --- a/openfisca_core/projectors/entity_to_person_projector.py +++ b/openfisca_core/projectors/entity_to_person_projector.py @@ -4,7 +4,7 @@ class EntityToPersonProjector(Projector): """For instance person.family.""" - def __init__(self, entity, parent=None): + def __init__(self, entity, parent=None) -> None: self.reference_entity = entity self.parent = parent diff --git a/openfisca_core/projectors/first_person_to_entity_projector.py b/openfisca_core/projectors/first_person_to_entity_projector.py index 4b4e7b7994..d986460cdc 100644 --- a/openfisca_core/projectors/first_person_to_entity_projector.py +++ b/openfisca_core/projectors/first_person_to_entity_projector.py @@ -4,7 +4,7 @@ class FirstPersonToEntityProjector(Projector): """For instance famille.first_person.""" - def __init__(self, entity, parent=None): + def __init__(self, entity, parent=None) -> None: self.target_entity = entity self.reference_entity = entity.members self.parent = parent diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index b3b7e6f2d3..1b3961c266 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -1,17 +1,19 @@ from __future__ import annotations -from collections.abc import Mapping - -from openfisca_core.types import GroupEntity, Role, SingleEntity +from typing import TYPE_CHECKING from openfisca_core import entities, projectors -from .typing import GroupPopulation, Population +if TYPE_CHECKING: + from collections.abc import Mapping + + from openfisca_core.types import GroupEntity, Role, SingleEntity + + from .typing import GroupPopulation, Population def projectable(function): - """ - Decorator to indicate that when called on a projector, the outcome of the function must be projected. + """Decorator to indicate that when called on a projector, the outcome of the function must be projected. For instance person.household.sum(...) must be projected on person, while it would not make sense for person.household.get_holder. """ function.projectable = True @@ -109,15 +111,15 @@ def get_projector_from_shortcut( <...UniqueRoleToEntityProjector object at ...> """ - entity: SingleEntity | GroupEntity = population.entity if isinstance(entity, entities.Entity): populations: Mapping[ - str, Population | GroupPopulation + str, + Population | GroupPopulation, ] = population.simulation.populations - if shortcut not in populations.keys(): + if shortcut not in populations: return None return projectors.EntityToPersonProjector(populations[shortcut], parent) @@ -133,7 +135,8 @@ def get_projector_from_shortcut( if shortcut in entity.containing_entities: projector: projectors.Projector = getattr( - projectors.FirstPersonToEntityProjector(population, parent), shortcut + projectors.FirstPersonToEntityProjector(population, parent), + shortcut, ) return projector diff --git a/openfisca_core/projectors/projector.py b/openfisca_core/projectors/projector.py index 5ab5f6d958..37881201dc 100644 --- a/openfisca_core/projectors/projector.py +++ b/openfisca_core/projectors/projector.py @@ -7,7 +7,9 @@ class Projector: def __getattr__(self, attribute): projector = helpers.get_projector_from_shortcut( - self.reference_entity, attribute, parent=self + self.reference_entity, + attribute, + parent=self, ) if projector: return projector @@ -30,8 +32,7 @@ def transform_and_bubble_up(self, result): transformed_result = self.transform(result) if self.parent is None: return transformed_result - else: - return self.parent.transform_and_bubble_up(transformed_result) + return self.parent.transform_and_bubble_up(transformed_result) def transform(self, result): return NotImplementedError() diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py index 186f90e30c..56e50e7230 100644 --- a/openfisca_core/projectors/typing.py +++ b/openfisca_core/projectors/typing.py @@ -1,32 +1,29 @@ from __future__ import annotations -from collections.abc import Mapping -from typing import Protocol +from typing import TYPE_CHECKING, Protocol -from openfisca_core.types import GroupEntity, SingleEntity +if TYPE_CHECKING: + from collections.abc import Mapping + + from openfisca_core.types import GroupEntity, SingleEntity class Population(Protocol): @property - def entity(self) -> SingleEntity: - ... + def entity(self) -> SingleEntity: ... @property - def simulation(self) -> Simulation: - ... + def simulation(self) -> Simulation: ... class GroupPopulation(Protocol): @property - def entity(self) -> GroupEntity: - ... + def entity(self) -> GroupEntity: ... @property - def simulation(self) -> Simulation: - ... + def simulation(self) -> Simulation: ... class Simulation(Protocol): @property - def populations(self) -> Mapping[str, Population | GroupPopulation]: - ... + def populations(self) -> Mapping[str, Population | GroupPopulation]: ... diff --git a/openfisca_core/projectors/unique_role_to_entity_projector.py b/openfisca_core/projectors/unique_role_to_entity_projector.py index fed2f249ca..c565484339 100644 --- a/openfisca_core/projectors/unique_role_to_entity_projector.py +++ b/openfisca_core/projectors/unique_role_to_entity_projector.py @@ -4,7 +4,7 @@ class UniqueRoleToEntityProjector(Projector): """For instance famille.declarant_principal.""" - def __init__(self, entity, role, parent=None): + def __init__(self, entity, role, parent=None) -> None: self.target_entity = entity self.reference_entity = entity.members self.parent = parent diff --git a/openfisca_core/reforms/reform.py b/openfisca_core/reforms/reform.py index 8c179596ed..76e7152334 100644 --- a/openfisca_core/reforms/reform.py +++ b/openfisca_core/reforms/reform.py @@ -7,23 +7,22 @@ class Reform(TaxBenefitSystem): - """A modified TaxBenefitSystem + """A modified TaxBenefitSystem. All reforms must subclass `Reform` and implement a method `apply()`. In this method, the reform can add or replace variables and call `modify_parameters` to modify the parameters of the legislation. - Example: - + Example: >>> from openfisca_core import reforms >>> from openfisca_core.parameters import load_parameter_file >>> >>> def modify_my_parameters(parameters): - >>> # Add new parameters + >>> # Add new parameters >>> new_parameters = load_parameter_file(name='reform_name', file_path='path_to_yaml_file.yaml') >>> parameters.add_child('reform_name', new_parameters) >>> - >>> # Update a value + >>> # Update a value >>> parameters.taxes.some_tax.some_param.update(period=some_period, value=1000.0) >>> >>> return parameters @@ -33,14 +32,13 @@ class Reform(TaxBenefitSystem): >>> self.add_variable(some_variable) >>> self.update_variable(some_other_variable) >>> self.modify_parameters(modifier_function = modify_my_parameters) + """ name = None - def __init__(self, baseline): - """ - :param baseline: Baseline TaxBenefitSystem. - """ + def __init__(self, baseline) -> None: + """:param baseline: Baseline TaxBenefitSystem.""" super().__init__(baseline.entities) self.baseline = baseline self.parameters = baseline.parameters @@ -49,9 +47,8 @@ def __init__(self, baseline): self.decomposition_file_path = baseline.decomposition_file_path self.key = self.__class__.__name__ if not hasattr(self, "apply"): - raise Exception( - "Reform {} must define an `apply` function".format(self.key) - ) + msg = f"Reform {self.key} must define an `apply` function" + raise Exception(msg) self.apply() def __getattr__(self, attribute): @@ -60,12 +57,12 @@ def __getattr__(self, attribute): @property def full_key(self): key = self.key - assert key is not None, "key was not set for reform {} (name: {!r})".format( - self, self.name - ) + assert ( + key is not None + ), f"key was not set for reform {self} (name: {self.name!r})" if self.baseline is not None and hasattr(self.baseline, "key"): baseline_full_key = self.baseline.full_key - key = ".".join([baseline_full_key, key]) + key = f"{baseline_full_key}.{key}" return key def modify_parameters(self, modifier_function): @@ -75,16 +72,15 @@ def modify_parameters(self, modifier_function): Args: modifier_function: A function that takes a :obj:`.ParameterNode` and should return an object of the same type. + """ baseline_parameters = self.baseline.parameters baseline_parameters_copy = copy.deepcopy(baseline_parameters) reform_parameters = modifier_function(baseline_parameters_copy) if not isinstance(reform_parameters, ParameterNode): return ValueError( - "modifier_function {} in module {} must return a ParameterNode".format( - modifier_function.__name__, - modifier_function.__module__, - ) + f"modifier_function {modifier_function.__name__} in module {modifier_function.__module__} must return a ParameterNode", ) self.parameters = reform_parameters self._parameters_at_instant_cache = {} + return None diff --git a/openfisca_core/scripts/__init__.py b/openfisca_core/scripts/__init__.py index 6366c8df15..e9080f2381 100644 --- a/openfisca_core/scripts/__init__.py +++ b/openfisca_core/scripts/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import importlib import logging import pkgutil @@ -17,7 +15,11 @@ def add_tax_benefit_system_arguments(parser): help='country package to use. If not provided, an automatic detection will be attempted by scanning the python packages installed in your environment which name contains the word "openfisca".', ) parser.add_argument( - "-e", "--extensions", action="store", help="extensions to load", nargs="*" + "-e", + "--extensions", + action="store", + help="extensions to load", + nargs="*", ) parser.add_argument( "-r", @@ -39,18 +41,17 @@ def build_tax_benefit_system(country_package_name, extensions, reforms): message = linesep.join( [ traceback.format_exc(), - "Could not import module `{}`.".format(country_package_name), + f"Could not import module `{country_package_name}`.", "Are you sure it is installed in your environment? If so, look at the stack trace above to determine the origin of this error.", "See more at .", - ] + ], ) raise ImportError(message) if not hasattr(country_package, "CountryTaxBenefitSystem"): + msg = f"`{country_package_name}` does not seem to be a valid Openfisca country package." raise ImportError( - "`{}` does not seem to be a valid Openfisca country package.".format( - country_package_name - ) + msg, ) country_package = importlib.import_module(country_package_name) @@ -82,22 +83,24 @@ def detect_country_package(): message = linesep.join( [ traceback.format_exc(), - "Could not import module `{}`.".format(module_name), + f"Could not import module `{module_name}`.", "Look at the stack trace above to determine the error that stopped installed modules detection.", - ] + ], ) raise ImportError(message) if hasattr(module, "CountryTaxBenefitSystem"): installed_country_packages.append(module_name) if len(installed_country_packages) == 0: + msg = "No country package has been detected on your environment. If your country package is installed but not detected, please use the --country-package option." raise ImportError( - "No country package has been detected on your environment. If your country package is installed but not detected, please use the --country-package option." + msg, ) if len(installed_country_packages) > 1: log.warning( "Several country packages detected : `{}`. Using `{}` by default. To use another package, please use the --country-package option.".format( - ", ".join(installed_country_packages), installed_country_packages[0] - ) + ", ".join(installed_country_packages), + installed_country_packages[0], + ), ) return installed_country_packages[0] diff --git a/openfisca_core/scripts/find_placeholders.py b/openfisca_core/scripts/find_placeholders.py index 37f31f6727..b7b5a81969 100644 --- a/openfisca_core/scripts/find_placeholders.py +++ b/openfisca_core/scripts/find_placeholders.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # flake8: noqa T001 import fnmatch @@ -10,7 +9,7 @@ def find_param_files(input_dir): param_files = [] - for root, dirnames, filenames in os.walk(input_dir): + for root, _dirnames, filenames in os.walk(input_dir): for filename in fnmatch.filter(filenames, "*.xml"): param_files.append(os.path.join(root, filename)) @@ -18,7 +17,7 @@ def find_param_files(input_dir): def find_placeholders(filename_input): - with open(filename_input, "r") as f: + with open(filename_input) as f: xml_content = f.read() xml_parsed = BeautifulSoup(xml_content, "lxml-xml") @@ -29,26 +28,17 @@ def find_placeholders(filename_input): for placeholder in placeholders: parent_list = list(placeholder.parents)[:-1] path = ".".join( - [p.attrs["code"] for p in parent_list if "code" in p.attrs][::-1] + [p.attrs["code"] for p in parent_list if "code" in p.attrs][::-1], ) deb = placeholder.attrs["deb"] output_list.append((deb, path)) - output_list = sorted(output_list, key=lambda x: x[0]) - - return output_list + return sorted(output_list, key=lambda x: x[0]) if __name__ == "__main__": - print( - """find_placeholders.py : Find nodes PLACEHOLDER in xml parameter files -Usage : - python find_placeholders /dir/to/search -""" - ) - assert len(sys.argv) == 2 input_dir = sys.argv[1] @@ -57,9 +47,5 @@ def find_placeholders(filename_input): for filename_input in param_files: output_list = find_placeholders(filename_input) - print("File {}".format(filename_input)) - - for deb, path in output_list: - print("{} {}".format(deb, path)) - - print("\n") + for _deb, _path in output_list: + pass diff --git a/openfisca_core/scripts/measure_numpy_condition_notations.py b/openfisca_core/scripts/measure_numpy_condition_notations.py index f737413bf4..65e48f6e2c 100755 --- a/openfisca_core/scripts/measure_numpy_condition_notations.py +++ b/openfisca_core/scripts/measure_numpy_condition_notations.py @@ -1,16 +1,15 @@ #! /usr/bin/env python -# -*- coding: utf-8 -*- # flake8: noqa T001 -""" -Measure and compare different vectorial condition notations: +"""Measure and compare different vectorial condition notations: - using multiplication notation: (choice == 1) * choice_1_value + (choice == 2) * choice_2_value - using numpy.select: the same than multiplication but more idiomatic like a "switch" control-flow statement -- using numpy.fromiter: iterates in Python over the array and calculates lazily only the required values +- using numpy.fromiter: iterates in Python over the array and calculates lazily only the required values. The aim of this script is to compare the time taken by the calculation of the values """ + import argparse import sys import time @@ -23,10 +22,9 @@ @contextmanager def measure_time(title): - t1 = time.time() + time.time() yield - t2 = time.time() - print("{}\t: {:.8f} seconds elapsed".format(title, t2 - t1)) + time.time() def switch_fromiter(conditions, function_by_condition, dtype): @@ -45,21 +43,21 @@ def get_or_store_value(condition): def switch_select(conditions, value_by_condition): - condlist = [conditions == condition for condition in value_by_condition.keys()] + condlist = [conditions == condition for condition in value_by_condition] return numpy.select(condlist, value_by_condition.values()) -def calculate_choice_1_value(): +def calculate_choice_1_value() -> int: time.sleep(args.calculate_time) return 80 -def calculate_choice_2_value(): +def calculate_choice_2_value() -> int: time.sleep(args.calculate_time) return 90 -def calculate_choice_3_value(): +def calculate_choice_3_value() -> int: time.sleep(args.calculate_time) return 95 @@ -68,32 +66,30 @@ def test_multiplication(choice): choice_1_value = calculate_choice_1_value() choice_2_value = calculate_choice_2_value() choice_3_value = calculate_choice_3_value() - result = ( + return ( (choice == 1) * choice_1_value + (choice == 2) * choice_2_value + (choice == 3) * choice_3_value ) - return result def test_switch_fromiter(choice): - result = switch_fromiter( + return switch_fromiter( choice, { 1: calculate_choice_1_value, 2: calculate_choice_2_value, 3: calculate_choice_3_value, }, - dtype=numpy.int, + dtype=int, ) - return result def test_switch_select(choice): choice_1_value = calculate_choice_1_value() choice_2_value = calculate_choice_2_value() choice_3_value = calculate_choice_2_value() - result = switch_select( + return switch_select( choice, { 1: choice_1_value, @@ -101,10 +97,9 @@ def test_switch_select(choice): 3: choice_3_value, }, ) - return result -def test_all_notations(): +def test_all_notations() -> None: # choice is an array with 1 and 2 items like [2, 1, ..., 1, 2] choice = numpy.random.randint(2, size=args.array_length) + 1 @@ -118,10 +113,13 @@ def test_all_notations(): test_switch_fromiter(choice) -def main(): +def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( - "--array-length", default=1000, type=int, help="length of the array" + "--array-length", + default=1000, + type=int, + help="length of the array", ) parser.add_argument( "--calculate-time", @@ -132,7 +130,6 @@ def main(): global args args = parser.parse_args() - print(args) test_all_notations() diff --git a/openfisca_core/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index 89fd47b441..48b99c93f8 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -1,15 +1,15 @@ #! /usr/bin/env python -# -*- coding: utf-8 -*- # flake8: noqa T001 """Measure performances of a basic tax-benefit system to compare to other OpenFisca implementations.""" + import argparse import logging import sys import time -import numpy as np +import numpy from numpy.core.defchararray import startswith from openfisca_core import periods, simulations @@ -24,11 +24,9 @@ def timeit(method): def timed(*args, **kwargs): - start_time = time.time() - result = method(*args, **kwargs) + time.time() + return method(*args, **kwargs) # print '%r (%r, %r) %2.9f s' % (method.__name__, args, kw, time.time() - start_time) - print("{:2.6f} s".format(time.time() - start_time)) - return result return timed @@ -106,7 +104,7 @@ def formula(self, simulation, period): if age_en_mois is not None: return age_en_mois // 12 birth = simulation.calculate("birth", period) - return (np.datetime64(period.date) - birth).astype("timedelta64[Y]") + return (numpy.datetime64(period.date) - birth).astype("timedelta64[Y]") class dom_tom(Variable): @@ -117,7 +115,9 @@ class dom_tom(Variable): def formula(self, simulation, period): period = period.start.period(DateUnit.YEAR).offset("first-of") city_code = simulation.calculate("city_code", period) - return np.logical_or(startswith(city_code, "97"), startswith(city_code, "98")) + return numpy.logical_or( + startswith(city_code, "97"), startswith(city_code, "98") + ) class revenu_disponible(Variable): @@ -158,10 +158,10 @@ class salaire_imposable(Variable): entity = Individu label = "Salaire imposable" - def formula(individu, period): + def formula(self, period): period = period.start.period(DateUnit.YEAR).offset("first-of") - dom_tom = individu.famille("dom_tom", period) - salaire_net = individu("salaire_net", period) + dom_tom = self.famille("dom_tom", period) + salaire_net = self("salaire_net", period) return salaire_net * 0.9 - 100 * dom_tom @@ -195,9 +195,10 @@ def formula(self, simulation, period): @timeit -def check_revenu_disponible(year, city_code, expected_revenu_disponible): +def check_revenu_disponible(year, city_code, expected_revenu_disponible) -> None: simulation = simulations.Simulation( - period=periods.period(year), tax_benefit_system=tax_benefit_system + period=periods.period(year), + tax_benefit_system=tax_benefit_system, ) famille = simulation.populations["famille"] famille.count = 3 @@ -206,20 +207,22 @@ def check_revenu_disponible(year, city_code, expected_revenu_disponible): individu = simulation.populations["individu"] individu.count = 6 individu.step_size = 2 - simulation.get_or_new_holder("city_code").array = np.array( - [city_code, city_code, city_code] + simulation.get_or_new_holder("city_code").array = numpy.array( + [city_code, city_code, city_code], ) - famille.members_entity_id = np.array([0, 0, 1, 1, 2, 2]) - simulation.get_or_new_holder("salaire_brut").array = np.array( - [0.0, 0.0, 50000.0, 0.0, 100000.0, 0.0] + famille.members_entity_id = numpy.array([0, 0, 1, 1, 2, 2]) + simulation.get_or_new_holder("salaire_brut").array = numpy.array( + [0.0, 0.0, 50000.0, 0.0, 100000.0, 0.0], ) revenu_disponible = simulation.calculate("revenu_disponible") assert_near( - revenu_disponible, expected_revenu_disponible, absolute_error_margin=0.005 + revenu_disponible, + expected_revenu_disponible, + absolute_error_margin=0.005, ) -def main(): +def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-v", @@ -231,37 +234,56 @@ def main(): global args args = parser.parse_args() logging.basicConfig( - level=logging.DEBUG if args.verbose else logging.WARNING, stream=sys.stdout + level=logging.DEBUG if args.verbose else logging.WARNING, + stream=sys.stdout, ) - check_revenu_disponible(2009, "75101", np.array([0, 0, 25200, 0, 50400, 0])) + check_revenu_disponible(2009, "75101", numpy.array([0, 0, 25200, 0, 50400, 0])) check_revenu_disponible( - 2010, "75101", np.array([1200, 1200, 25200, 1200, 50400, 1200]) + 2010, + "75101", + numpy.array([1200, 1200, 25200, 1200, 50400, 1200]), ) check_revenu_disponible( - 2011, "75101", np.array([2400, 2400, 25200, 2400, 50400, 2400]) + 2011, + "75101", + numpy.array([2400, 2400, 25200, 2400, 50400, 2400]), ) check_revenu_disponible( - 2012, "75101", np.array([2400, 2400, 25200, 2400, 50400, 2400]) + 2012, + "75101", + numpy.array([2400, 2400, 25200, 2400, 50400, 2400]), ) check_revenu_disponible( - 2013, "75101", np.array([3600, 3600, 25200, 3600, 50400, 3600]) + 2013, + "75101", + numpy.array([3600, 3600, 25200, 3600, 50400, 3600]), ) check_revenu_disponible( - 2009, "97123", np.array([-70.0, -70.0, 25130.0, -70.0, 50330.0, -70.0]) + 2009, + "97123", + numpy.array([-70.0, -70.0, 25130.0, -70.0, 50330.0, -70.0]), ) check_revenu_disponible( - 2010, "97123", np.array([1130.0, 1130.0, 25130.0, 1130.0, 50330.0, 1130.0]) + 2010, + "97123", + numpy.array([1130.0, 1130.0, 25130.0, 1130.0, 50330.0, 1130.0]), ) check_revenu_disponible( - 2011, "98456", np.array([2330.0, 2330.0, 25130.0, 2330.0, 50330.0, 2330.0]) + 2011, + "98456", + numpy.array([2330.0, 2330.0, 25130.0, 2330.0, 50330.0, 2330.0]), ) check_revenu_disponible( - 2012, "98456", np.array([2330.0, 2330.0, 25130.0, 2330.0, 50330.0, 2330.0]) + 2012, + "98456", + numpy.array([2330.0, 2330.0, 25130.0, 2330.0, 50330.0, 2330.0]), ) check_revenu_disponible( - 2013, "98456", np.array([3530.0, 3530.0, 25130.0, 3530.0, 50330.0, 3530.0]) + 2013, + "98456", + numpy.array([3530.0, 3530.0, 25130.0, 3530.0, 50330.0, 3530.0]), ) diff --git a/openfisca_core/scripts/measure_performances_fancy_indexing.py b/openfisca_core/scripts/measure_performances_fancy_indexing.py index 030b1af7aa..7c261e2fe3 100644 --- a/openfisca_core/scripts/measure_performances_fancy_indexing.py +++ b/openfisca_core/scripts/measure_performances_fancy_indexing.py @@ -2,24 +2,25 @@ import timeit -import numpy as np +import numpy from openfisca_france import CountryTaxBenefitSystem tbs = CountryTaxBenefitSystem() N = 200000 al_plaf_acc = tbs.get_parameters_at_instant("2015-01-01").prestations.al_plaf_acc -zone_apl = np.random.choice([1, 2, 3], N) -al_nb_pac = np.random.choice(6, N) -couple = np.random.choice([True, False], N) +zone_apl = numpy.random.choice([1, 2, 3], N) +al_nb_pac = numpy.random.choice(6, N) +couple = numpy.random.choice([True, False], N) formatted_zone = concat( - "plafond_pour_accession_a_la_propriete_zone_", zone_apl + "plafond_pour_accession_a_la_propriete_zone_", + zone_apl, ) # zone_apl returns 1, 2 or 3 but the parameters have a long name def formula_with(): plafonds = al_plaf_acc[formatted_zone] - result = ( + return ( plafonds.personne_isolee_sans_enfant * not_(couple) * (al_nb_pac == 0) + plafonds.menage_seul * couple * (al_nb_pac == 0) + plafonds.menage_ou_isole_avec_1_enfant * (al_nb_pac == 1) @@ -32,8 +33,6 @@ def formula_with(): * (al_nb_pac - 5) ) - return result - def formula_without(): z1 = al_plaf_acc.plafond_pour_accession_a_la_propriete_zone_1 @@ -79,14 +78,12 @@ def formula_without(): if __name__ == "__main__": time_with = timeit.timeit( - "formula_with()", setup="from __main__ import formula_with", number=50 + "formula_with()", + setup="from __main__ import formula_with", + number=50, ) time_without = timeit.timeit( - "formula_without()", setup="from __main__ import formula_without", number=50 - ) - - print("Computing with dynamic legislation computing took {}".format(time_with)) - print( - "Computing without dynamic legislation computing took {}".format(time_without) + "formula_without()", + setup="from __main__ import formula_without", + number=50, ) - print("Ratio: {}".format(time_with / time_without)) diff --git a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py index 3ff9c3d7ac..38538d644a 100644 --- a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +++ b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py @@ -1,12 +1,11 @@ -# -*- coding: utf-8 -*- - -""" xml_to_yaml_country_template.py : Parse XML parameter files for Country-Template and convert them to YAML files. Comments are NOT transformed. +"""xml_to_yaml_country_template.py : Parse XML parameter files for Country-Template and convert them to YAML files. Comments are NOT transformed. Usage : `python xml_to_yaml_country_template.py output_dir` or just (output is written in a directory called `yaml_parameters`): `python xml_to_yaml_country_template.py` """ + import os import sys @@ -16,10 +15,7 @@ tax_benefit_system = CountryTaxBenefitSystem() -if len(sys.argv) > 1: - target_path = sys.argv[1] -else: - target_path = "yaml_parameters" +target_path = sys.argv[1] if len(sys.argv) > 1 else "yaml_parameters" param_dir = os.path.join(COUNTRY_DIR, "parameters") param_files = [ diff --git a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py index 34b7ca430d..0b57c19016 100644 --- a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +++ b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- - -""" xml_to_yaml_extension_template.py : Parse XML parameter files for Extension-Template and convert them to YAML files. Comments are NOT transformed. +"""xml_to_yaml_extension_template.py : Parse XML parameter files for Extension-Template and convert them to YAML files. Comments are NOT transformed. Usage : `python xml_to_yaml_extension_template.py output_dir` @@ -15,10 +13,7 @@ from . import xml_to_yaml -if len(sys.argv) > 1: - target_path = sys.argv[1] -else: - target_path = "yaml_parameters" +target_path = sys.argv[1] if len(sys.argv) > 1 else "yaml_parameters" param_dir = os.path.dirname(openfisca_extension_template.__file__) param_files = [ diff --git a/openfisca_core/scripts/migrations/v24_to_25.py b/openfisca_core/scripts/migrations/v24_to_25.py index 1eefd426ad..08bbeddc3b 100644 --- a/openfisca_core/scripts/migrations/v24_to_25.py +++ b/openfisca_core/scripts/migrations/v24_to_25.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # flake8: noqa T001 import argparse @@ -33,21 +32,21 @@ def build_parser(): parser = argparse.ArgumentParser() parser.add_argument( - "path", help="paths (files or directories) of tests to execute", nargs="+" + "path", + help="paths (files or directories) of tests to execute", + nargs="+", ) - parser = add_tax_benefit_system_arguments(parser) + return add_tax_benefit_system_arguments(parser) - return parser - -class Migrator(object): - def __init__(self, tax_benefit_system): +class Migrator: + def __init__(self, tax_benefit_system) -> None: self.tax_benefit_system = tax_benefit_system self.entities_by_plural = { entity.plural: entity for entity in self.tax_benefit_system.entities } - def migrate(self, path): + def migrate(self, path) -> None: if isinstance(path, list): for item in path: self.migrate(item) @@ -65,8 +64,6 @@ def migrate(self, path): return - print("Migrating {}.".format(path)) - with open(path) as yaml_file: tests = yaml.safe_load(yaml_file) if isinstance(tests, CommentedSeq): @@ -107,14 +104,12 @@ def convert_inputs(self, inputs): continue results[entity_plural] = self.convert_entities(entity, entities_description) - results = self.generate_missing_entities(results) - - return results + return self.generate_missing_entities(results) def convert_entities(self, entity, entities_description): return { - entity_description.get("id", "{}_{}".format(entity.key, index)): remove_id( - entity_description + entity_description.get("id", f"{entity.key}_{index}"): remove_id( + entity_description, ) for index, entity_description in enumerate(entities_description) } @@ -127,12 +122,12 @@ def generate_missing_entities(self, inputs): if len(persons) == 1: person_id = next(iter(persons)) inputs[entity.key] = { - entity.roles[0].plural or entity.roles[0].key: [person_id] + entity.roles[0].plural or entity.roles[0].key: [person_id], } else: inputs[entity.plural] = { - "{}_{}".format(entity.key, index): { - entity.roles[0].plural or entity.roles[0].key: [person_id] + f"{entity.key}_{index}": { + entity.roles[0].plural or entity.roles[0].key: [person_id], } for index, person_id in enumerate(persons.keys()) } @@ -143,13 +138,15 @@ def remove_id(input_dict): return {key: value for (key, value) in input_dict.items() if key != "id"} -def main(): +def main() -> None: parser = build_parser() args = parser.parse_args() paths = [os.path.abspath(path) for path in args.path] tax_benefit_system = build_tax_benefit_system( - args.country_package, args.extensions, args.reforms + args.country_package, + args.extensions, + args.reforms, ) Migrator(tax_benefit_system).migrate(paths) diff --git a/openfisca_core/scripts/openfisca_command.py b/openfisca_core/scripts/openfisca_command.py index 3b835e73a3..d82e0aef61 100644 --- a/openfisca_core/scripts/openfisca_command.py +++ b/openfisca_core/scripts/openfisca_command.py @@ -30,7 +30,10 @@ def build_serve_parser(parser): type=int, ) parser.add_argument( - "--tracker-url", action="store", help="tracking service url", type=str + "--tracker-url", + action="store", + help="tracking service url", + type=str, ) parser.add_argument( "--tracker-idsite", @@ -65,7 +68,9 @@ def build_serve_parser(parser): def build_test_parser(parser): parser.add_argument( - "path", help="paths (files or directories) of tests to execute", nargs="+" + "path", + help="paths (files or directories) of tests to execute", + nargs="+", ) parser = add_tax_benefit_system_arguments(parser) parser.add_argument( @@ -156,6 +161,7 @@ def main(): from openfisca_core.scripts.run_test import main return sys.exit(main(parser)) + return None if __name__ == "__main__": diff --git a/openfisca_core/scripts/remove_fuzzy.py b/openfisca_core/scripts/remove_fuzzy.py index 2c06b149b1..a4827aef39 100755 --- a/openfisca_core/scripts/remove_fuzzy.py +++ b/openfisca_core/scripts/remove_fuzzy.py @@ -10,7 +10,7 @@ assert len(sys.argv) == 2 filename = sys.argv[1] -with open(filename, "r") as f: +with open(filename) as f: lines = f.readlines() diff --git a/openfisca_core/scripts/run_test.py b/openfisca_core/scripts/run_test.py index ab292c4165..458dc7e50e 100644 --- a/openfisca_core/scripts/run_test.py +++ b/openfisca_core/scripts/run_test.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import logging import os import sys @@ -8,14 +6,17 @@ from openfisca_core.tools.test_runner import run_tests -def main(parser): +def main(parser) -> None: args = parser.parse_args() logging.basicConfig( - level=logging.DEBUG if args.verbose else logging.WARNING, stream=sys.stdout + level=logging.DEBUG if args.verbose else logging.WARNING, + stream=sys.stdout, ) tax_benefit_system = build_tax_benefit_system( - args.country_package, args.extensions, args.reforms + args.country_package, + args.extensions, + args.reforms, ) options = { diff --git a/openfisca_core/scripts/simulation_generator.py b/openfisca_core/scripts/simulation_generator.py index 4f2c5bfd4f..eca2fa30d1 100644 --- a/openfisca_core/scripts/simulation_generator.py +++ b/openfisca_core/scripts/simulation_generator.py @@ -6,21 +6,22 @@ def make_simulation(tax_benefit_system, nb_persons, nb_groups, **kwargs): - """ - Generate a simulation containing nb_persons persons spread in nb_groups groups. + """Generate a simulation containing nb_persons persons spread in nb_groups groups. Example: - >>> from openfisca_core.scripts.simulation_generator import make_simulation >>> from openfisca_france import CountryTaxBenefitSystem >>> tbs = CountryTaxBenefitSystem() - >>> simulation = make_simulation(tbs, 400, 100) # Create a simulation with 400 persons, spread among 100 families - >>> simulation.calculate('revenu_disponible', 2017) + >>> simulation = make_simulation( + ... tbs, 400, 100 + ... ) # Create a simulation with 400 persons, spread among 100 families + >>> simulation.calculate("revenu_disponible", 2017) + """ simulation = Simulation(tax_benefit_system=tax_benefit_system, **kwargs) simulation.persons.ids = numpy.arange(nb_persons) simulation.persons.count = nb_persons - adults = [0] + sorted(random.sample(range(1, nb_persons), nb_groups - 1)) + adults = [0, *sorted(random.sample(range(1, nb_persons), nb_groups - 1))] members_entity_id = numpy.empty(nb_persons, dtype=int) @@ -50,26 +51,40 @@ def make_simulation(tax_benefit_system, nb_persons, nb_groups, **kwargs): def randomly_init_variable( - simulation, variable_name: str, period, max_value, condition=None -): - """ - Initialise a variable with random values (from 0 to max_value) for the given period. + simulation, + variable_name: str, + period, + max_value, + condition=None, +) -> None: + """Initialise a variable with random values (from 0 to max_value) for the given period. If a condition vector is provided, only set the value of persons or groups for which condition is True. Example: - - >>> from openfisca_core.scripts.simulation_generator import make_simulation, randomly_init_variable + >>> from openfisca_core.scripts.simulation_generator import ( + ... make_simulation, + ... randomly_init_variable, + ... ) >>> from openfisca_france import CountryTaxBenefitSystem >>> tbs = CountryTaxBenefitSystem() - >>> simulation = make_simulation(tbs, 400, 100) # Create a simulation with 400 persons, spread among 100 families - >>> randomly_init_variable(simulation, 'salaire_net', 2017, max_value = 50000, condition = simulation.persons.has_role(simulation.famille.DEMANDEUR)) # Randomly set a salaire_net for all persons between 0 and 50000? - >>> simulation.calculate('revenu_disponible', 2017) + >>> simulation = make_simulation( + ... tbs, 400, 100 + ... ) # Create a simulation with 400 persons, spread among 100 families + >>> randomly_init_variable( + ... simulation, + ... "salaire_net", + ... 2017, + ... max_value=50000, + ... condition=simulation.persons.has_role(simulation.famille.DEMANDEUR), + ... ) # Randomly set a salaire_net for all persons between 0 and 50000? + >>> simulation.calculate("revenu_disponible", 2017) + """ if condition is None: condition = True variable = simulation.tax_benefit_system.get_variable(variable_name) population = simulation.get_variable_population(variable_name) value = (numpy.random.rand(population.count) * max_value * condition).astype( - variable.dtype + variable.dtype, ) simulation.set_input(variable_name, period, value) diff --git a/openfisca_core/simulations/__init__.py b/openfisca_core/simulations/__init__.py index 670b922ebb..9ab10f81a7 100644 --- a/openfisca_core/simulations/__init__.py +++ b/openfisca_core/simulations/__init__.py @@ -21,20 +21,16 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from openfisca_core.errors import ( # noqa: F401 - CycleError, - NaNCreationError, - SpiralError, -) +from openfisca_core.errors import CycleError, NaNCreationError, SpiralError -from .helpers import ( # noqa: F401 +from .helpers import ( calculate_output_add, calculate_output_divide, check_type, transform_to_strict_syntax, ) -from .simulation import Simulation # noqa: F401 -from .simulation_builder import SimulationBuilder # noqa: F401 +from .simulation import Simulation +from .simulation_builder import SimulationBuilder __all__ = [ "CycleError", diff --git a/openfisca_core/simulations/_build_default_simulation.py b/openfisca_core/simulations/_build_default_simulation.py index f99c1d210a..adc7cf4783 100644 --- a/openfisca_core/simulations/_build_default_simulation.py +++ b/openfisca_core/simulations/_build_default_simulation.py @@ -27,9 +27,9 @@ class _BuildDefaultSimulation: >>> count = 1 >>> builder = ( ... _BuildDefaultSimulation(tax_benefit_system, count) - ... .add_count() - ... .add_ids() - ... .add_members_entity_id() + ... .add_count() + ... .add_ids() + ... .add_members_entity_id() ... ) >>> builder.count @@ -84,7 +84,6 @@ def add_count(self) -> Self: 2 """ - for population in self.populations.values(): population.count = self.count @@ -117,7 +116,6 @@ def add_ids(self) -> Self: array([0, 1]) """ - for population in self.populations.values(): population.ids = numpy.array(range(self.count)) @@ -154,7 +152,6 @@ def add_members_entity_id(self) -> Self: array([0, 1]) """ - for population in self.populations.values(): if hasattr(population, "members_entity_id"): population.members_entity_id = numpy.array(range(self.count)) diff --git a/openfisca_core/simulations/_build_from_variables.py b/openfisca_core/simulations/_build_from_variables.py index 60ff6148e7..5819a8aa2b 100644 --- a/openfisca_core/simulations/_build_from_variables.py +++ b/openfisca_core/simulations/_build_from_variables.py @@ -2,14 +2,17 @@ from __future__ import annotations +from typing import TYPE_CHECKING from typing_extensions import Self from openfisca_core import errors from ._build_default_simulation import _BuildDefaultSimulation from ._type_guards import is_variable_dated -from .simulation import Simulation -from .typing import Entity, Population, TaxBenefitSystem, Variables + +if TYPE_CHECKING: + from .simulation import Simulation + from .typing import Entity, Population, TaxBenefitSystem, Variables class _BuildFromVariables: @@ -139,7 +142,6 @@ def add_dated_values(self) -> Self: >>> pack.get_array(period) """ - for variable, value in self.variables.items(): if is_variable_dated(dated_variable := value): for period, dated_value in dated_variable.items(): @@ -197,7 +199,6 @@ def add_undated_values(self) -> Self: array([5000], dtype=int32) """ - for variable, value in self.variables.items(): if not is_variable_dated(undated_value := value): if (period := self.default_period) is None: diff --git a/openfisca_core/simulations/_type_guards.py b/openfisca_core/simulations/_type_guards.py index c34361041a..8f31a7a2e8 100644 --- a/openfisca_core/simulations/_type_guards.py +++ b/openfisca_core/simulations/_type_guards.py @@ -2,22 +2,26 @@ from __future__ import annotations -from typing import Iterable +from typing import TYPE_CHECKING from typing_extensions import TypeGuard -from .typing import ( - Axes, - DatedVariable, - FullySpecifiedEntities, - ImplicitGroupEntities, - Params, - UndatedVariable, - Variables, -) +if TYPE_CHECKING: + from collections.abc import Iterable + + from .typing import ( + Axes, + DatedVariable, + FullySpecifiedEntities, + ImplicitGroupEntities, + Params, + UndatedVariable, + Variables, + ) def are_entities_fully_specified( - params: Params, items: Iterable[str] + params: Params, + items: Iterable[str], ) -> TypeGuard[FullySpecifiedEntities]: """Check if the params contain fully specified entities. @@ -33,28 +37,32 @@ def are_entities_fully_specified( >>> params = { ... "axes": [ - ... [{"count": 2, "max": 3000, "min": 0, "name": "rent", "period": "2018-11"}] + ... [ + ... { + ... "count": 2, + ... "max": 3000, + ... "min": 0, + ... "name": "rent", + ... "period": "2018-11", + ... } + ... ] ... ], ... "households": { ... "housea": {"parents": ["Alicia", "Javier"]}, ... "houseb": {"parents": ["Tom"]}, - ... }, + ... }, ... "persons": {"Alicia": {"salary": {"2018-11": 0}}, "Javier": {}, "Tom": {}}, ... } >>> are_entities_fully_specified(params, entities) True - >>> params = { - ... "persons": {"Javier": {"salary": {"2018-11": [2000, 3000]}}} - ... } + >>> params = {"persons": {"Javier": {"salary": {"2018-11": [2000, 3000]}}}} >>> are_entities_fully_specified(params, entities) True - >>> params = { - ... "persons": {"Javier": {"salary": {"2018-11": 2000}}} - ... } + >>> params = {"persons": {"Javier": {"salary": {"2018-11": 2000}}}} >>> are_entities_fully_specified(params, entities) True @@ -80,15 +88,15 @@ def are_entities_fully_specified( False """ - if not params: return False - return all(key in items for key in params.keys() if key != "axes") + return all(key in items for key in params if key != "axes") def are_entities_short_form( - params: Params, items: Iterable[str] + params: Params, + items: Iterable[str], ) -> TypeGuard[ImplicitGroupEntities]: """Check if the params contain short form entities. @@ -103,25 +111,23 @@ def are_entities_short_form( >>> entities = {"person", "household"} >>> params = { - ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, ... "households": {"household": {"parents": ["Javier"]}}, - ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]], ... } >>> are_entities_short_form(params, entities) False - >>> params = { - ... "persons": {"Javier": {"salary": {"2018-11": 2000}}} - ... } + >>> params = {"persons": {"Javier": {"salary": {"2018-11": 2000}}}} >>> are_entities_short_form(params, entities) False >>> params = { - ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, ... "household": {"parents": ["Javier"]}, - ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]], ... } >>> are_entities_short_form(params, entities) @@ -129,7 +135,7 @@ def are_entities_short_form( >>> params = { ... "household": {"parents": ["Javier"]}, - ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]], ... } >>> are_entities_short_form(params, entities) @@ -161,12 +167,12 @@ def are_entities_short_form( False """ - - return not not set(params).intersection(items) + return bool(set(params).intersection(items)) def are_entities_specified( - params: Params, items: Iterable[str] + params: Params, + items: Iterable[str], ) -> TypeGuard[Variables]: """Check if the params contains entities at all. @@ -181,24 +187,20 @@ def are_entities_specified( >>> variables = {"salary"} >>> params = { - ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, ... "households": {"household": {"parents": ["Javier"]}}, - ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]], ... } >>> are_entities_specified(params, variables) True - >>> params = { - ... "persons": {"Javier": {"salary": {"2018-11": [2000, 3000]}}} - ... } + >>> params = {"persons": {"Javier": {"salary": {"2018-11": [2000, 3000]}}}} >>> are_entities_specified(params, variables) True - >>> params = { - ... "persons": {"Javier": {"salary": {"2018-11": 2000}}} - ... } + >>> params = {"persons": {"Javier": {"salary": {"2018-11": 2000}}}} >>> are_entities_specified(params, variables) True @@ -234,11 +236,10 @@ def are_entities_specified( False """ - if not params: return False - return not any(key in items for key in params.keys()) + return not any(key in items for key in params) def has_axes(params: Params) -> TypeGuard[Axes]: @@ -252,23 +253,20 @@ def has_axes(params: Params) -> TypeGuard[Axes]: Examples: >>> params = { - ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, ... "households": {"household": {"parents": ["Javier"]}}, - ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]], ... } >>> has_axes(params) True - >>> params = { - ... "persons": {"Javier": {"salary": {"2018-11": [2000, 3000]}}} - ... } + >>> params = {"persons": {"Javier": {"salary": {"2018-11": [2000, 3000]}}}} >>> has_axes(params) False """ - return params.get("axes", None) is not None @@ -300,5 +298,4 @@ def is_variable_dated( False """ - return isinstance(variable, dict) diff --git a/openfisca_core/simulations/helpers.py b/openfisca_core/simulations/helpers.py index d5984d88b6..7929c5beda 100644 --- a/openfisca_core/simulations/helpers.py +++ b/openfisca_core/simulations/helpers.py @@ -13,7 +13,7 @@ def calculate_output_divide(simulation, variable_name: str, period): return simulation.calculate_divide(variable_name, period) -def check_type(input, input_type, path=None): +def check_type(input, input_type, path=None) -> None: json_type_map = { dict: "Object", list: "Array", @@ -26,12 +26,13 @@ def check_type(input, input_type, path=None): if not isinstance(input, input_type): raise errors.SituationParsingError( path, - "Invalid type: must be of type '{}'.".format(json_type_map[input_type]), + f"Invalid type: must be of type '{json_type_map[input_type]}'.", ) def check_unexpected_entities( - params: ParamsWithoutAxes, entities: Iterable[str] + params: ParamsWithoutAxes, + entities: Iterable[str], ) -> None: """Check if the input contains entities that are not in the system. @@ -47,21 +48,18 @@ def check_unexpected_entities( >>> params = { ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, - ... "households": {"household": {"parents": ["Javier"]}} + ... "households": {"household": {"parents": ["Javier"]}}, ... } >>> check_unexpected_entities(params, entities) - >>> params = { - ... "dogs": {"Bart": {"damages": {"2018-11": 2000}}} - ... } + >>> params = {"dogs": {"Bart": {"damages": {"2018-11": 2000}}}} >>> check_unexpected_entities(params, entities) Traceback (most recent call last): openfisca_core.errors.situation_parsing_error.SituationParsingError """ - if has_unexpected_entities(params, entities): unexpected_entities = [entity for entity in params if entity not in entities] @@ -90,21 +88,18 @@ def has_unexpected_entities(params: ParamsWithoutAxes, entities: Iterable[str]) >>> params = { ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, - ... "households": {"household": {"parents": ["Javier"]}} + ... "households": {"household": {"parents": ["Javier"]}}, ... } >>> has_unexpected_entities(params, entities) False - >>> params = { - ... "dogs": {"Bart": {"damages": {"2018-11": 2000}}} - ... } + >>> params = {"dogs": {"Bart": {"damages": {"2018-11": 2000}}}} >>> has_unexpected_entities(params, entities) True """ - return any(entity for entity in params if entity not in entities) diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 93becda960..259e676e36 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,34 +1,40 @@ from __future__ import annotations -from typing import Dict, Mapping, NamedTuple, Optional, Set - -from openfisca_core.types import Population, TaxBenefitSystem, Variable +from typing import TYPE_CHECKING, NamedTuple import tempfile import warnings import numpy -from openfisca_core import commons, errors, indexed_enums, periods, tracers -from openfisca_core import warnings as core_warnings +from openfisca_core import ( + commons, + errors, + indexed_enums, + periods, + tracers, + warnings as core_warnings, +) + +if TYPE_CHECKING: + from collections.abc import Mapping + + from openfisca_core.types import Population, TaxBenefitSystem, Variable class Simulation: - """ - Represents a simulation, and handles the calculation logic - """ + """Represents a simulation, and handles the calculation logic.""" tax_benefit_system: TaxBenefitSystem - populations: Dict[str, Population] - invalidated_caches: Set[Cache] + populations: dict[str, Population] + invalidated_caches: set[Cache] def __init__( self, tax_benefit_system: TaxBenefitSystem, populations: Mapping[str, Population], - ): - """ - This constructor is reserved for internal use; see :any:`SimulationBuilder`, + ) -> None: + """This constructor is reserved for internal use; see :any:`SimulationBuilder`, which is the preferred way to obtain a Simulation initialized with a consistent set of Entities. """ @@ -57,37 +63,37 @@ def trace(self): return self._trace @trace.setter - def trace(self, trace): + def trace(self, trace) -> None: self._trace = trace if trace: self.tracer = tracers.FullTracer() else: self.tracer = tracers.SimpleTracer() - def link_to_entities_instances(self): - for _key, entity_instance in self.populations.items(): + def link_to_entities_instances(self) -> None: + for entity_instance in self.populations.values(): entity_instance.simulation = self - def create_shortcuts(self): - for _key, population in self.populations.items(): + def create_shortcuts(self) -> None: + for population in self.populations.values(): # create shortcut simulation.person and simulation.household (for instance) setattr(self, population.entity.key, population) @property def data_storage_dir(self): - """ - Temporary folder used to store intermediate calculation data in case the memory is saturated - """ + """Temporary folder used to store intermediate calculation data in case the memory is saturated.""" if self._data_storage_dir is None: self._data_storage_dir = tempfile.mkdtemp(prefix="openfisca_") message = [ ( - "Intermediate results will be stored on disk in {} in case of memory overflow." - ).format(self._data_storage_dir), + f"Intermediate results will be stored on disk in {self._data_storage_dir} in case of memory overflow." + ), "You should remove this directory once you're done with your simulation.", ] warnings.warn( - " ".join(message), core_warnings.TempfileWarning, stacklevel=2 + " ".join(message), + core_warnings.TempfileWarning, + stacklevel=2, ) return self._data_storage_dir @@ -95,7 +101,6 @@ def data_storage_dir(self): def calculate(self, variable_name: str, period): """Calculate ``variable_name`` for ``period``.""" - if period is not None and not isinstance(period, periods.Period): period = periods.period(period) @@ -111,17 +116,17 @@ def calculate(self, variable_name: str, period): self.purge_cache_of_invalid_values() def _calculate(self, variable_name: str, period: periods.Period): - """ - Calculate the variable ``variable_name`` for the period ``period``, using the variable formula if it exists. + """Calculate the variable ``variable_name`` for the period ``period``, using the variable formula if it exists. :returns: A numpy array containing the result of the calculation """ - variable: Optional[Variable] + variable: Variable | None population = self.get_variable_population(variable_name) holder = population.get_holder(variable_name) variable = self.tax_benefit_system.get_variable( - variable_name, check_existence=True + variable_name, + check_existence=True, ) if variable is None: @@ -153,7 +158,7 @@ def _calculate(self, variable_name: str, period: periods.Period): return array - def purge_cache_of_invalid_values(self): + def purge_cache_of_invalid_values(self) -> None: # We wait for the end of calculate(), signalled by an empty stack, before purging the cache if self.tracer.stack: return @@ -163,10 +168,11 @@ def purge_cache_of_invalid_values(self): self.invalidated_caches = set() def calculate_add(self, variable_name: str, period): - variable: Optional[Variable] + variable: Variable | None variable = self.tax_benefit_system.get_variable( - variable_name, check_existence=True + variable_name, + check_existence=True, ) if variable is None: @@ -177,23 +183,29 @@ def calculate_add(self, variable_name: str, period): # Check that the requested period matches definition_period if periods.unit_weight(variable.definition_period) > periods.unit_weight( - period.unit + period.unit, ): - raise ValueError( + msg = ( f"Unable to compute variable '{variable.name}' for period " f"{period}: '{variable.name}' can only be computed for " f"{variable.definition_period}-long periods. You can use the " f"DIVIDE option to get an estimate of {variable.name}." ) + raise ValueError( + msg, + ) if variable.definition_period not in ( periods.DateUnit.isoformat + periods.DateUnit.isocalendar ): - raise ValueError( + msg = ( f"Unable to ADD constant variable '{variable.name}' over " f"the period {period}: eternal variables can't be summed " "over time." ) + raise ValueError( + msg, + ) return sum( self.calculate(variable_name, sub_period) @@ -201,10 +213,11 @@ def calculate_add(self, variable_name: str, period): ) def calculate_divide(self, variable_name: str, period): - variable: Optional[Variable] + variable: Variable | None variable = self.tax_benefit_system.get_variable( - variable_name, check_existence=True + variable_name, + check_existence=True, ) if variable is None: @@ -218,32 +231,41 @@ def calculate_divide(self, variable_name: str, period): < periods.unit_weight(period.unit) or period.size > 1 ): - raise ValueError( + msg = ( f"Can't calculate variable '{variable.name}' for period " f"{period}: '{variable.name}' can only be computed for " f"{variable.definition_period}-long periods. You can use the " f"ADD option to get an estimate of {variable.name}." ) + raise ValueError( + msg, + ) if variable.definition_period not in ( periods.DateUnit.isoformat + periods.DateUnit.isocalendar ): - raise ValueError( + msg = ( f"Unable to DIVIDE constant variable '{variable.name}' over " f"the period {period}: eternal variables can't be divided " "over time." ) + raise ValueError( + msg, + ) if ( period.unit not in (periods.DateUnit.isoformat + periods.DateUnit.isocalendar) or period.size != 1 ): - raise ValueError( + msg = ( f"Unable to DIVIDE constant variable '{variable.name}' over " f"the period {period}: eternal variables can't be used " "as a denominator to divide a variable over time." ) + raise ValueError( + msg, + ) if variable.definition_period == periods.DateUnit.YEAR: calculation_period = period.this_year @@ -278,14 +300,12 @@ def calculate_divide(self, variable_name: str, period): return self.calculate(variable_name, calculation_period) / denominator def calculate_output(self, variable_name: str, period): - """ - Calculate the value of a variable using the ``calculate_output`` attribute of the variable. - """ - - variable: Optional[Variable] + """Calculate the value of a variable using the ``calculate_output`` attribute of the variable.""" + variable: Variable | None variable = self.tax_benefit_system.get_variable( - variable_name, check_existence=True + variable_name, + check_existence=True, ) if variable is None: @@ -303,10 +323,7 @@ def trace_parameters_at_instant(self, formula_period): ) def _run_formula(self, variable, population, period): - """ - Find the ``variable`` formula for the given ``period`` if it exists, and apply it to ``population``. - """ - + """Find the ``variable`` formula for the given ``period`` if it exists, and apply it to ``population``.""" formula = variable.get_formula(period) if formula is None: return None @@ -323,10 +340,8 @@ def _run_formula(self, variable, population, period): return array - def _check_period_consistency(self, period, variable): - """ - Check that a period matches the variable definition_period - """ + def _check_period_consistency(self, period, variable) -> None: + """Check that a period matches the variable definition_period.""" if variable.definition_period == periods.DateUnit.ETERNITY: return # For variables which values are constant in time, all periods are accepted @@ -334,42 +349,39 @@ def _check_period_consistency(self, period, variable): variable.definition_period == periods.DateUnit.YEAR and period.unit != periods.DateUnit.YEAR ): + msg = f"Unable to compute variable '{variable.name}' for period {period}: '{variable.name}' must be computed for a whole year. You can use the DIVIDE option to get an estimate of {variable.name} by dividing the yearly value by 12, or change the requested period to 'period.this_year'." raise ValueError( - "Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole year. You can use the DIVIDE option to get an estimate of {0} by dividing the yearly value by 12, or change the requested period to 'period.this_year'.".format( - variable.name, period - ) + msg, ) if ( variable.definition_period == periods.DateUnit.MONTH and period.unit != periods.DateUnit.MONTH ): + msg = f"Unable to compute variable '{variable.name}' for period {period}: '{variable.name}' must be computed for a whole month. You can use the ADD option to sum '{variable.name}' over the requested period, or change the requested period to 'period.first_month'." raise ValueError( - "Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole month. You can use the ADD option to sum '{0}' over the requested period, or change the requested period to 'period.first_month'.".format( - variable.name, period - ) + msg, ) if ( variable.definition_period == periods.DateUnit.WEEK and period.unit != periods.DateUnit.WEEK ): + msg = f"Unable to compute variable '{variable.name}' for period {period}: '{variable.name}' must be computed for a whole week. You can use the ADD option to sum '{variable.name}' over the requested period, or change the requested period to 'period.first_week'." raise ValueError( - "Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole week. You can use the ADD option to sum '{0}' over the requested period, or change the requested period to 'period.first_week'.".format( - variable.name, period - ) + msg, ) if period.size != 1: + msg = f"Unable to compute variable '{variable.name}' for period {period}: '{variable.name}' must be computed for a whole {variable.definition_period}. You can use the ADD option to sum '{variable.name}' over the requested period." raise ValueError( - "Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole {2}. You can use the ADD option to sum '{0}' over the requested period.".format( - variable.name, period, variable.definition_period - ) + msg, ) def _cast_formula_result(self, value, variable): if variable.value_type == indexed_enums.Enum and not isinstance( - value, indexed_enums.EnumArray + value, + indexed_enums.EnumArray, ): return variable.possible_values.encode(value) @@ -384,9 +396,8 @@ def _cast_formula_result(self, value, variable): # ----- Handle circular dependencies in a calculation ----- # - def _check_for_cycle(self, variable: str, period): - """ - Raise an exception in the case of a circular definition, where evaluating a variable for + def _check_for_cycle(self, variable: str, period) -> None: + """Raise an exception in the case of a circular definition, where evaluating a variable for a given period loops around to evaluating the same variable/period pair. Also guards, as a heuristic, against "quasicircles", where the evaluation of a variable at a period involves the same variable at a different period. @@ -398,21 +409,20 @@ def _check_for_cycle(self, variable: str, period): if frame["name"] == variable ] if period in previous_periods: + msg = f"Circular definition detected on formula {variable}@{period}" raise errors.CycleError( - "Circular definition detected on formula {}@{}".format(variable, period) + msg, ) spiral = len(previous_periods) >= self.max_spiral_loops if spiral: self.invalidate_spiral_variables(variable) - message = "Quasicircular definition detected on formula {}@{} involving {}".format( - variable, period, self.tracer.stack - ) + message = f"Quasicircular definition detected on formula {variable}@{period} involving {self.tracer.stack}" raise errors.SpiralError(message, variable) - def invalidate_cache_entry(self, variable: str, period): + def invalidate_cache_entry(self, variable: str, period) -> None: self.invalidated_caches.add(Cache(variable, period)) - def invalidate_spiral_variables(self, variable: str): + def invalidate_spiral_variables(self, variable: str) -> None: # Visit the stack, from the bottom (most recent) up; we know that we'll find # the variable implicated in the spiral (max_spiral_loops+1) times; we keep the # intermediate values computed (to avoid impacting performance) but we mark them @@ -428,8 +438,7 @@ def invalidate_spiral_variables(self, variable: str): # ----- Methods to access stored values ----- # def get_array(self, variable_name: str, period): - """ - Return the value of ``variable_name`` for ``period``, if this value is alreay in the cache (if it has been set as an input or previously calculated). + """Return the value of ``variable_name`` for ``period``, if this value is alreay in the cache (if it has been set as an input or previously calculated). Unlike :meth:`.calculate`, this method *does not* trigger calculations and *does not* use any formula. """ @@ -442,10 +451,8 @@ def get_holder(self, variable_name: str): return self.get_variable_population(variable_name).get_holder(variable_name) def get_memory_usage(self, variables=None): - """ - Get data about the virtual memory usage of the simulation - """ - result = dict(total_nb_bytes=0, by_variable={}) + """Get data about the virtual memory usage of the simulation.""" + result = {"total_nb_bytes": 0, "by_variable": {}} for entity in self.populations.values(): entity_memory_usage = entity.get_memory_usage(variables=variables) result["total_nb_bytes"] += entity_memory_usage["total_nb_bytes"] @@ -454,55 +461,52 @@ def get_memory_usage(self, variables=None): # ----- Misc ----- # - def delete_arrays(self, variable, period=None): - """ - Delete a variable's value for a given period + def delete_arrays(self, variable, period=None) -> None: + """Delete a variable's value for a given period. :param variable: the variable to be set :param period: the period for which the value should be deleted Example: - >>> from openfisca_country_template import CountryTaxBenefitSystem >>> simulation = Simulation(CountryTaxBenefitSystem()) - >>> simulation.set_input('age', '2018-04', [12, 14]) - >>> simulation.set_input('age', '2018-05', [13, 14]) - >>> simulation.get_array('age', '2018-05') + >>> simulation.set_input("age", "2018-04", [12, 14]) + >>> simulation.set_input("age", "2018-05", [13, 14]) + >>> simulation.get_array("age", "2018-05") array([13, 14], dtype=int32) - >>> simulation.delete_arrays('age', '2018-05') - >>> simulation.get_array('age', '2018-04') + >>> simulation.delete_arrays("age", "2018-05") + >>> simulation.get_array("age", "2018-04") array([12, 14], dtype=int32) - >>> simulation.get_array('age', '2018-05') is None + >>> simulation.get_array("age", "2018-05") is None True - >>> simulation.set_input('age', '2018-05', [13, 14]) - >>> simulation.delete_arrays('age') - >>> simulation.get_array('age', '2018-04') is None + >>> simulation.set_input("age", "2018-05", [13, 14]) + >>> simulation.delete_arrays("age") + >>> simulation.get_array("age", "2018-04") is None True - >>> simulation.get_array('age', '2018-05') is None + >>> simulation.get_array("age", "2018-05") is None True + """ self.get_holder(variable).delete_arrays(period) def get_known_periods(self, variable): - """ - Get a list variable's known period, i.e. the periods where a value has been initialized and + """Get a list variable's known period, i.e. the periods where a value has been initialized and. :param variable: the variable to be set Example: - >>> from openfisca_country_template import CountryTaxBenefitSystem >>> simulation = Simulation(CountryTaxBenefitSystem()) - >>> simulation.set_input('age', '2018-04', [12, 14]) - >>> simulation.set_input('age', '2018-05', [13, 14]) - >>> simulation.get_known_periods('age') + >>> simulation.set_input("age", "2018-04", [12, 14]) + >>> simulation.set_input("age", "2018-05", [13, 14]) + >>> simulation.get_known_periods("age") [Period((u'month', Instant((2018, 5, 1)), 1)), Period((u'month', Instant((2018, 4, 1)), 1))] + """ return self.get_holder(variable).get_known_periods() - def set_input(self, variable_name: str, period, value): - """ - Set a variable's value for a given period + def set_input(self, variable_name: str, period, value) -> None: + """Set a variable's value for a given period. :param variable: the variable to be set :param value: the input value for the variable @@ -511,16 +515,18 @@ def set_input(self, variable_name: str, period, value): Example: >>> from openfisca_country_template import CountryTaxBenefitSystem >>> simulation = Simulation(CountryTaxBenefitSystem()) - >>> simulation.set_input('age', '2018-04', [12, 14]) - >>> simulation.get_array('age', '2018-04') + >>> simulation.set_input("age", "2018-04", [12, 14]) + >>> simulation.get_array("age", "2018-04") array([12, 14], dtype=int32) If a ``set_input`` property has been set for the variable, this method may accept inputs for periods not matching the ``definition_period`` of the variable. To read more about this, check the `documentation `_. + """ - variable: Optional[Variable] + variable: Variable | None variable = self.tax_benefit_system.get_variable( - variable_name, check_existence=True + variable_name, + check_existence=True, ) if variable is None: @@ -532,10 +538,11 @@ def set_input(self, variable_name: str, period, value): self.get_holder(variable_name).set_input(period, value) def get_variable_population(self, variable_name: str) -> Population: - variable: Optional[Variable] + variable: Variable | None variable = self.tax_benefit_system.get_variable( - variable_name, check_existence=True + variable_name, + check_existence=True, ) if variable is None: @@ -543,7 +550,7 @@ def get_variable_population(self, variable_name: str) -> Population: return self.populations[variable.entity.key] - def get_population(self, plural: Optional[str] = None) -> Optional[Population]: + def get_population(self, plural: str | None = None) -> Population | None: return next( ( population @@ -555,8 +562,8 @@ def get_population(self, plural: Optional[str] = None) -> Optional[Population]: def get_entity( self, - plural: Optional[str] = None, - ) -> Optional[Population]: + plural: str | None = None, + ) -> Population | None: population = self.get_population(plural) return population and population.entity @@ -567,9 +574,7 @@ def describe_entities(self): } def clone(self, debug=False, trace=False): - """ - Copy the simulation just enough to be able to run the copy without modifying the original simulation - """ + """Copy the simulation just enough to be able to run the copy without modifying the original simulation.""" new = commons.empty_clone(self) new_dict = new.__dict__ @@ -585,7 +590,9 @@ def clone(self, debug=False, trace=False): population = self.populations[entity.key].clone(new) new.populations[entity.key] = population setattr( - new, entity.key, population + new, + entity.key, + population, ) # create shortcut simulation.household (for instance) new.debug = debug diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index c42d0e4f22..854e99e958 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -1,8 +1,6 @@ from __future__ import annotations -from collections.abc import Iterable, Sequence -from numpy.typing import NDArray as Array -from typing import Dict, List +from typing import TYPE_CHECKING, NoReturn import copy @@ -21,25 +19,30 @@ has_axes, ) from .simulation import Simulation -from .typing import ( - Axis, - Entity, - FullySpecifiedEntities, - GroupEntities, - GroupEntity, - ImplicitGroupEntities, - Params, - ParamsWithoutAxes, - Population, - Role, - SingleEntity, - TaxBenefitSystem, - Variables, -) + +if TYPE_CHECKING: + from collections.abc import Iterable, Sequence + from numpy.typing import NDArray as Array + + from .typing import ( + Axis, + Entity, + FullySpecifiedEntities, + GroupEntities, + GroupEntity, + ImplicitGroupEntities, + Params, + ParamsWithoutAxes, + Population, + Role, + SingleEntity, + TaxBenefitSystem, + Variables, + ) class SimulationBuilder: - def __init__(self): + def __init__(self) -> None: self.default_period = ( None # Simulation period used for variables when no period is defined ) @@ -48,26 +51,27 @@ def __init__(self): ) # JSON input - Memory of known input values. Indexed by variable or axis name. - self.input_buffer: Dict[ - variables.Variable.name, Dict[str(periods.period), numpy.array] + self.input_buffer: dict[ + variables.Variable.name, + dict[str(periods.period), numpy.array], ] = {} - self.populations: Dict[entities.Entity.key, populations.Population] = {} + self.populations: dict[entities.Entity.key, populations.Population] = {} # JSON input - Number of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_ids``, including axes. - self.entity_counts: Dict[entities.Entity.plural, int] = {} + self.entity_counts: dict[entities.Entity.plural, int] = {} # JSON input - List of items of each entity type. Indexed by entities plural names. Should be consistent with ``entity_counts``. - self.entity_ids: Dict[entities.Entity.plural, List[int]] = {} + self.entity_ids: dict[entities.Entity.plural, list[int]] = {} # Links entities with persons. For each person index in persons ids list, set entity index in entity ids id. E.g.: self.memberships[entity.plural][person_index] = entity_ids.index(instance_id) - self.memberships: Dict[entities.Entity.plural, List[int]] = {} - self.roles: Dict[entities.Entity.plural, List[int]] = {} + self.memberships: dict[entities.Entity.plural, list[int]] = {} + self.roles: dict[entities.Entity.plural, list[int]] = {} - self.variable_entities: Dict[variables.Variable.name, entities.Entity] = {} + self.variable_entities: dict[variables.Variable.name, entities.Entity] = {} self.axes = [[]] - self.axes_entity_counts: Dict[entities.Entity.plural, int] = {} - self.axes_entity_ids: Dict[entities.Entity.plural, List[int]] = {} - self.axes_memberships: Dict[entities.Entity.plural, List[int]] = {} - self.axes_roles: Dict[entities.Entity.plural, List[int]] = {} + self.axes_entity_counts: dict[entities.Entity.plural, int] = {} + self.axes_entity_ids: dict[entities.Entity.plural, list[int]] = {} + self.axes_memberships: dict[entities.Entity.plural, list[int]] = {} + self.axes_roles: dict[entities.Entity.plural, list[int]] = {} def build_from_dict( self, @@ -91,9 +95,9 @@ def build_from_dict( >>> entities = {"person", "household"} >>> params = { - ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, ... "household": {"parents": ["Javier"]}, - ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]], ... } >>> are_entities_short_form(params, entities) @@ -103,13 +107,25 @@ def build_from_dict( >>> params = { ... "axes": [ - ... [{"count": 2, "max": 3000, "min": 0, "name": "rent", "period": "2018-11"}] + ... [ + ... { + ... "count": 2, + ... "max": 3000, + ... "min": 0, + ... "name": "rent", + ... "period": "2018-11", + ... } + ... ] ... ], ... "households": { ... "housea": {"parents": ["Alicia", "Javier"]}, ... "houseb": {"parents": ["Tom"]}, - ... }, - ... "persons": {"Alicia": {"salary": {"2018-11": 0}}, "Javier": {}, "Tom": {}}, + ... }, + ... "persons": { + ... "Alicia": {"salary": {"2018-11": 0}}, + ... "Javier": {}, + ... "Tom": {}, + ... }, ... } >>> are_entities_short_form(params, entities) @@ -121,7 +137,6 @@ def build_from_dict( True """ - #: The plural names of the entities in the tax and benefits system. plural: Iterable[str] = tax_benefit_system.entities_plural() @@ -140,6 +155,7 @@ def build_from_dict( if not are_entities_specified(params := input_dict, variables): return self.build_from_variables(tax_benefit_system, params) + return None def build_from_entities( self, @@ -153,16 +169,15 @@ def build_from_entities( >>> entities = {"person", "household"} >>> params = { - ... "persons": {"Javier": { "salary": { "2018-11": 2000}}}, + ... "persons": {"Javier": {"salary": {"2018-11": 2000}}}, ... "household": {"parents": ["Javier"]}, - ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]] + ... "axes": [[{"count": 1, "max": 1, "min": 1, "name": "household"}]], ... } >>> are_entities_short_form(params, entities) True """ - # Create the populations populations = tax_benefit_system.instantiate_entities() @@ -203,9 +218,7 @@ def build_from_entities( if not persons_json: raise errors.SituationParsingError( [person_entity.plural], - "No {0} found. At least one {0} must be defined to run a simulation.".format( - person_entity.key - ), + f"No {person_entity.key} found. At least one {person_entity.key} must be defined to run a simulation.", ) persons_ids = self.add_person_entity(simulation.persons.entity, persons_json) @@ -215,7 +228,10 @@ def build_from_entities( if instances_json is not None: self.add_group_entity( - self.persons_plural, persons_ids, entity_class, instances_json + self.persons_plural, + persons_ids, + entity_class, + instances_json, ) elif axes is not None: @@ -257,7 +273,9 @@ def build_from_entities( return simulation def build_from_variables( - self, tax_benefit_system: TaxBenefitSystem, input_dict: Variables + self, + tax_benefit_system: TaxBenefitSystem, + input_dict: Variables, ) -> Simulation: """Build a simulation from a Python dict ``input_dict`` describing variables values without expliciting entities. @@ -276,18 +294,17 @@ def build_from_variables( SituationParsingError: If the input is not valid. Examples: - >>> params = {'salary': {'2016-10': 12000}} + >>> params = {"salary": {"2016-10": 12000}} >>> are_entities_specified(params, {"salary"}) False - >>> params = {'salary': 12000} + >>> params = {"salary": 12000} >>> are_entities_specified(params, {"salary"}) False """ - return ( _BuildFromVariables(tax_benefit_system, input_dict, self.default_period) .add_dated_values() @@ -297,7 +314,8 @@ def build_from_variables( @staticmethod def build_default_simulation( - tax_benefit_system: TaxBenefitSystem, count: int = 1 + tax_benefit_system: TaxBenefitSystem, + count: int = 1, ) -> Simulation: """Build a default simulation. @@ -307,7 +325,6 @@ def build_default_simulation( - Every person has, in each entity, the first role """ - return ( _BuildDefaultSimulation(tax_benefit_system, count) .add_count() @@ -316,10 +333,10 @@ def build_default_simulation( .simulation ) - def create_entities(self, tax_benefit_system): + def create_entities(self, tax_benefit_system) -> None: self.populations = tax_benefit_system.instantiate_entities() - def declare_person_entity(self, person_singular, persons_ids: Iterable): + def declare_person_entity(self, person_singular, persons_ids: Iterable) -> None: person_instance = self.populations[person_singular] person_instance.ids = numpy.array(list(persons_ids)) person_instance.count = len(person_instance.ids) @@ -336,11 +353,15 @@ def nb_persons(self, entity_singular, role=None): return self.populations[entity_singular].nb_persons(role=role) def join_with_persons( - self, group_population, persons_group_assignment, roles: Iterable[str] - ): + self, + group_population, + persons_group_assignment, + roles: Iterable[str], + ) -> None: # Maps group's identifiers to a 0-based integer range, for indexing into members_roles (see PR#876) group_sorted_indices = numpy.unique( - persons_group_assignment, return_inverse=True + persons_group_assignment, + return_inverse=True, )[1] group_population.members_entity_id = numpy.argsort(group_population.ids)[ group_sorted_indices @@ -350,27 +371,32 @@ def join_with_persons( roles_array = numpy.array(roles) if numpy.issubdtype(roles_array.dtype, numpy.integer): group_population.members_role = numpy.array(flattened_roles)[roles_array] + elif len(flattened_roles) == 0: + group_population.members_role = numpy.int64(0) else: - if len(flattened_roles) == 0: - group_population.members_role = numpy.int64(0) - else: - group_population.members_role = numpy.select( - [roles_array == role.key for role in flattened_roles], - flattened_roles, - ) + group_population.members_role = numpy.select( + [roles_array == role.key for role in flattened_roles], + flattened_roles, + ) def build(self, tax_benefit_system): return Simulation(tax_benefit_system, self.populations) def explicit_singular_entities( - self, tax_benefit_system: TaxBenefitSystem, input_dict: ImplicitGroupEntities + self, + tax_benefit_system: TaxBenefitSystem, + input_dict: ImplicitGroupEntities, ) -> GroupEntities: """Preprocess ``input_dict`` to explicit entities defined using the - single-entity shortcut + single-entity shortcut. Examples: - - >>> params = {'persons': {'Javier': {}, }, 'household': {'parents': ['Javier']}} + >>> params = { + ... "persons": { + ... "Javier": {}, + ... }, + ... "household": {"parents": ["Javier"]}, + ... } >>> are_entities_fully_specified(params, {"persons", "households"}) False @@ -378,7 +404,10 @@ def explicit_singular_entities( >>> are_entities_short_form(params, {"person", "household"}) True - >>> params = {'persons': {'Javier': {}}, 'households': {'household': {'parents': ['Javier']}}} + >>> params = { + ... "persons": {"Javier": {}}, + ... "households": {"household": {"parents": ["Javier"]}}, + ... } >>> are_entities_fully_specified(params, {"persons", "households"}) True @@ -387,9 +416,8 @@ def explicit_singular_entities( False """ - singular_keys = set(input_dict).intersection( - tax_benefit_system.entities_by_singular() + tax_benefit_system.entities_by_singular(), ) result = { @@ -405,9 +433,7 @@ def explicit_singular_entities( return result def add_person_entity(self, entity, instances_json): - """ - Add the simulation's instances of the persons entity as described in ``instances_json``. - """ + """Add the simulation's instances of the persons entity as described in ``instances_json``.""" helpers.check_type(instances_json, dict, [entity.plural]) entity_ids = list(map(str, instances_json.keys())) self.persons_plural = entity.plural @@ -421,14 +447,16 @@ def add_person_entity(self, entity, instances_json): return self.get_ids(entity.plural) def add_default_group_entity( - self, persons_ids: list[str], entity: GroupEntity + self, + persons_ids: list[str], + entity: GroupEntity, ) -> None: persons_count = len(persons_ids) roles = list(entity.flattened_roles) self.entity_ids[entity.plural] = persons_ids self.entity_counts[entity.plural] = persons_count self.memberships[entity.plural] = list( - numpy.arange(0, persons_count, dtype=numpy.int32) + numpy.arange(0, persons_count, dtype=numpy.int32), ) self.roles[entity.plural] = [roles[0]] * persons_count @@ -439,9 +467,7 @@ def add_group_entity( entity: GroupEntity, instances_json, ) -> None: - """ - Add all instances of one of the model's entities as described in ``instances_json``. - """ + """Add all instances of one of the model's entities as described in ``instances_json``.""" helpers.check_type(instances_json, dict, [entity.plural]) entity_ids = list(map(str, instances_json.keys())) @@ -464,14 +490,16 @@ def add_group_entity( roles_json = { role.plural or role.key: helpers.transform_to_strict_syntax( - variables_json.pop(role.plural or role.key, []) + variables_json.pop(role.plural or role.key, []), ) for role in entity.roles } for role_id, role_definition in roles_json.items(): helpers.check_type( - role_definition, list, [entity.plural, instance_id, role_id] + role_definition, + list, + [entity.plural, instance_id, role_id], ) for index, person_id in enumerate(role_definition): entity_plural = entity.plural @@ -515,7 +543,7 @@ def add_group_entity( for person_id in persons_to_allocate: person_index = persons_ids.index(person_id) self.memberships[entity.plural][person_index] = entity_ids.index( - person_id + person_id, ) self.roles[entity.plural][person_index] = entity.flattened_roles[0] # Adjust previously computed ids and counts @@ -526,7 +554,7 @@ def add_group_entity( self.roles[entity.plural] = self.roles[entity.plural].tolist() self.memberships[entity.plural] = self.memberships[entity.plural].tolist() - def set_default_period(self, period_str): + def set_default_period(self, period_str) -> None: if period_str: self.default_period = str(periods.period(period_str)) @@ -546,26 +574,24 @@ def check_persons_to_allocate( role_id, persons_to_allocate, index, - ): + ) -> None: helpers.check_type( - person_id, str, [entity_plural, entity_id, role_id, str(index)] + person_id, + str, + [entity_plural, entity_id, role_id, str(index)], ) if person_id not in persons_ids: raise errors.SituationParsingError( [entity_plural, entity_id, role_id], - "Unexpected value: {0}. {0} has been declared in {1} {2}, but has not been declared in {3}.".format( - person_id, entity_id, role_id, persons_plural - ), + f"Unexpected value: {person_id}. {person_id} has been declared in {entity_id} {role_id}, but has not been declared in {persons_plural}.", ) if person_id not in persons_to_allocate: raise errors.SituationParsingError( [entity_plural, entity_id, role_id], - "{} has been declared more than once in {}".format( - person_id, entity_plural - ), + f"{person_id} has been declared more than once in {entity_plural}", ) - def init_variable_values(self, entity, instance_object, instance_id): + def init_variable_values(self, entity, instance_object, instance_id) -> None: for variable_name, variable_values in instance_object.items(): path_in_json = [entity.plural, instance_id, variable_name] try: @@ -592,12 +618,23 @@ def init_variable_values(self, entity, instance_object, instance_id): raise errors.SituationParsingError(path_in_json, e.args[0]) variable = entity.get_variable(variable_name) self.add_variable_value( - entity, variable, instance_index, instance_id, period_str, value + entity, + variable, + instance_index, + instance_id, + period_str, + value, ) def add_variable_value( - self, entity, variable, instance_index, instance_id, period_str, value - ): + self, + entity, + variable, + instance_index, + instance_id, + period_str, + value, + ) -> None: path_in_json = [entity.plural, instance_id, variable.name, period_str] if value is None: @@ -618,7 +655,7 @@ def add_variable_value( self.input_buffer[variable.name][str(periods.period(period_str))] = array - def finalize_variables_init(self, population): + def finalize_variables_init(self, population) -> None: # Due to set_input mechanism, we must bufferize all inputs, then actually set them, # so that the months are set first and the years last. plural_key = population.entity.plural @@ -628,7 +665,7 @@ def finalize_variables_init(self, population): if plural_key in self.memberships: population.members_entity_id = numpy.array(self.get_memberships(plural_key)) population.members_role = numpy.array(self.get_roles(plural_key)) - for variable_name in self.input_buffer.keys(): + for variable_name in self.input_buffer: try: holder = population.get_holder(variable_name) except ValueError: # Wrong entity, we can just ignore that @@ -636,7 +673,7 @@ def finalize_variables_init(self, population): buffer = self.input_buffer[variable_name] unsorted_periods = [ periods.period(period_str) - for period_str in self.input_buffer[variable_name].keys() + for period_str in self.input_buffer[variable_name] ] # We need to handle small periods first for set_input to work sorted_periods = sorted(unsorted_periods, key=periods.key_period_size) @@ -651,21 +688,23 @@ def finalize_variables_init(self, population): if (variable.end is None) or (period_value.start.date <= variable.end): holder.set_input(period_value, array) - def raise_period_mismatch(self, entity, json, e): + def raise_period_mismatch(self, entity, json, e) -> NoReturn: # This error happens when we try to set a variable value for a period that doesn't match its definition period # It is only raised when we consume the buffer. We thus don't know which exact key caused the error. # We do a basic research to find the culprit path culprit_path = next( dpath.util.search( - json, f"*/{e.variable_name}/{str(e.period)}", yielded=True + json, + f"*/{e.variable_name}/{e.period!s}", + yielded=True, ), None, ) if culprit_path: - path = [entity.plural] + culprit_path[0].split("/") + path = [entity.plural, *culprit_path[0].split("/")] else: path = [ - entity.plural + entity.plural, ] # Fallback: if we can't find the culprit, just set the error at the entities level raise errors.SituationParsingError(path, e.message) @@ -682,7 +721,8 @@ def get_ids(self, entity_name: str) -> list[str]: def get_memberships(self, entity_name): # Return empty array for the "persons" entity return self.axes_memberships.get( - entity_name, self.memberships.get(entity_name, []) + entity_name, + self.memberships.get(entity_name, []), ) # Returns the roles of individuals in this entity, including when there is replication along axes @@ -710,7 +750,7 @@ def expand_axes(self) -> None: cell_count *= axis_count # Scale the "prototype" situation, repeating it cell_count times - for entity_name in self.entity_counts.keys(): + for entity_name in self.entity_counts: # Adjust counts self.axes_entity_counts[entity_name] = ( self.get_count(entity_name) * cell_count @@ -718,7 +758,8 @@ def expand_axes(self) -> None: # Adjust ids original_ids: list[str] = self.get_ids(entity_name) * cell_count indices: Array[numpy.int_] = numpy.arange( - 0, cell_count * self.entity_counts[entity_name] + 0, + cell_count * self.entity_counts[entity_name], ) adjusted_ids: list[str] = [ original_id + str(index) @@ -792,7 +833,7 @@ def expand_axes(self) -> None: array = self.get_input(axis_name, str(axis_period)) if array is None: array = variable.default_array( - cell_count * axis_entity_step_size + cell_count * axis_entity_step_size, ) elif array.size == axis_entity_step_size: array = numpy.tile(array, cell_count) diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py index 8603d0d811..8eda695384 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/typing.py @@ -3,19 +3,23 @@ from __future__ import annotations from collections.abc import Iterable, Sequence -from numpy.typing import NDArray as Array -from typing import Protocol, TypeVar, TypedDict, Union +from typing import TYPE_CHECKING, Protocol, TypeVar, TypedDict, Union from typing_extensions import NotRequired, Required, TypeAlias import datetime from abc import abstractmethod -from numpy import bool_ as Bool -from numpy import datetime64 as Date -from numpy import float32 as Float -from numpy import int16 as Enum -from numpy import int32 as Int -from numpy import str_ as String +from numpy import ( + bool_ as Bool, + datetime64 as Date, + float32 as Float, + int16 as Enum, + int32 as Int, + str_ as String, +) + +if TYPE_CHECKING: + from numpy.typing import NDArray as Array #: Generic type variables. E = TypeVar("E") @@ -54,7 +58,9 @@ #: Type alias for a simulation dictionary without axes parameters. ParamsWithoutAxes: TypeAlias = Union[ - Variables, ImplicitGroupEntities, FullySpecifiedEntities + Variables, + ImplicitGroupEntities, + FullySpecifiedEntities, ] #: Type alias for a simulation dictionary with axes parameters. diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 14b607feac..311143b412 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -1,9 +1,6 @@ from __future__ import annotations -import typing -from typing import Any, Dict, Optional, Sequence, Union - -from openfisca_core.types import ParameterNodeAtInstant +from typing import TYPE_CHECKING, Any import ast import copy @@ -20,7 +17,6 @@ import traceback from openfisca_core import commons, periods, variables -from openfisca_core.entities import Entity from openfisca_core.errors import VariableNameConflictError, VariableNotFoundError from openfisca_core.parameters import ParameterNode from openfisca_core.periods import Instant, Period @@ -28,6 +24,13 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable +if TYPE_CHECKING: + from collections.abc import Sequence + + from openfisca_core.types import ParameterNodeAtInstant + + from openfisca_core.entities import Entity + log = logging.getLogger(__name__) @@ -48,7 +51,7 @@ class TaxBenefitSystem: person_entity: Entity _base_tax_benefit_system = None - _parameters_at_instant_cache: Dict[Instant, ParameterNodeAtInstant] = {} + _parameters_at_instant_cache: dict[Instant, ParameterNodeAtInstant] = {} person_key_plural = None preprocess_parameters = None baseline = None # Baseline tax-benefit system. Used only by reforms. Note: Reforms can be chained. @@ -57,14 +60,17 @@ class TaxBenefitSystem: def __init__(self, entities: Sequence[Entity]) -> None: # TODO: Currently: Don't use a weakref, because they are cleared by Paste (at least) at each call. - self.parameters: Optional[ParameterNode] = None - self.variables: Dict[Any, Any] = {} - self.open_api_config: Dict[Any, Any] = {} + self.parameters: ParameterNode | None = None + self.variables: dict[Any, Any] = {} + self.open_api_config: dict[Any, Any] = {} # Tax benefit systems are mutable, so entities (which need to know about our variables) can't be shared among them if entities is None or len(entities) == 0: - raise Exception("A tax and benefit sytem must have at least an entity.") + msg = "A tax and benefit sytem must have at least an entity." + raise Exception(msg) self.entities = [copy.copy(entity) for entity in entities] - self.person_entity = [entity for entity in self.entities if entity.is_person][0] + self.person_entity = next( + entity for entity in self.entities if entity.is_person + ) self.group_entities = [ entity for entity in self.entities if not entity.is_person ] @@ -78,15 +84,15 @@ def base_tax_benefit_system(self): baseline = self.baseline if baseline is None: return self - self._base_tax_benefit_system = ( - base_tax_benefit_system - ) = baseline.base_tax_benefit_system + self._base_tax_benefit_system = base_tax_benefit_system = ( + baseline.base_tax_benefit_system + ) return base_tax_benefit_system def instantiate_entities(self): person = self.person_entity members = Population(person) - entities: typing.Dict[Entity.key, Entity] = {person.key: members} + entities: dict[Entity.key, Entity] = {person.key: members} for entity in self.group_entities: entities[entity.key] = GroupPopulation(entity, members) @@ -95,8 +101,8 @@ def instantiate_entities(self): # Deprecated method of constructing simulations, to be phased out in favor of SimulationBuilder def new_scenario(self): - class ScenarioAdapter(object): - def __init__(self, tax_benefit_system): + class ScenarioAdapter: + def __init__(self, tax_benefit_system) -> None: self.tax_benefit_system = tax_benefit_system def init_from_attributes(self, **attributes): @@ -110,7 +116,11 @@ def init_from_dict(self, dict): return self def new_simulation( - self, debug=False, opt_out_cache=False, use_baseline=False, trace=False + self, + debug=False, + opt_out_cache=False, + use_baseline=False, + trace=False, ): # Legacy from scenarios, used in reforms tax_benefit_system = self.tax_benefit_system @@ -127,12 +137,14 @@ def new_simulation( period = self.attributes.get("period") builder.set_default_period(period) simulation = builder.build_from_variables( - tax_benefit_system, variables + tax_benefit_system, + variables, ) else: builder.set_default_period(self.period) simulation = builder.build_from_entities( - tax_benefit_system, self.dict + tax_benefit_system, + self.dict, ) simulation.trace = trace @@ -143,7 +155,7 @@ def new_simulation( return ScenarioAdapter(self) - def prefill_cache(self): + def prefill_cache(self) -> None: pass def load_variable(self, variable_class, update=False): @@ -152,10 +164,9 @@ def load_variable(self, variable_class, update=False): # Check if a Variable with the same name is already registered. baseline_variable = self.get_variable(name) if baseline_variable and not update: + msg = f'Variable "{name}" is already defined. Use `update_variable` to replace it.' raise VariableNameConflictError( - 'Variable "{}" is already defined. Use `update_variable` to replace it.'.format( - name - ) + msg, ) variable = variable_class(baseline_variable=baseline_variable) @@ -213,16 +224,14 @@ def update_variable(self, variable: Variable) -> Variable: The added variable. """ - return self.load_variable(variable, update=True) - def add_variables_from_file(self, file_path): - """ - Adds all OpenFisca variables contained in a given file to the tax and benefit system. - """ + def add_variables_from_file(self, file_path) -> None: + """Adds all OpenFisca variables contained in a given file to the tax and benefit system.""" try: source_file_path = file_path.replace( - self.get_package_metadata()["location"], "" + self.get_package_metadata()["location"], + "", ) file_name = os.path.splitext(os.path.basename(file_path))[0] @@ -244,9 +253,9 @@ def add_variables_from_file(self, file_path): spec.loader.exec_module(module) except NameError as e: - logging.error( + logging.exception( str(e) - + ": if this code used to work, this error might be due to a major change in OpenFisca-Core. Checkout the changelog to learn more: " + + ": if this code used to work, this error might be due to a major change in OpenFisca-Core. Checkout the changelog to learn more: ", ) raise potential_variables = [ @@ -269,15 +278,11 @@ def add_variables_from_file(self, file_path): ) self.add_variable(pot_variable) except Exception: - log.error( - 'Unable to load OpenFisca variables from file "{}"'.format(file_path) - ) + log.exception(f'Unable to load OpenFisca variables from file "{file_path}"') raise - def add_variables_from_directory(self, directory): - """ - Recursively explores a directory, and adds all OpenFisca variables found there to the tax and benefit system. - """ + def add_variables_from_directory(self, directory) -> None: + """Recursively explores a directory, and adds all OpenFisca variables found there to the tax and benefit system.""" py_files = glob.glob(os.path.join(directory, "*.py")) for py_file in py_files: self.add_variables_from_file(py_file) @@ -285,18 +290,16 @@ def add_variables_from_directory(self, directory): for subdirectory in subdirectories: self.add_variables_from_directory(subdirectory) - def add_variables(self, *variables): - """ - Adds a list of OpenFisca Variables to the `TaxBenefitSystem`. + def add_variables(self, *variables) -> None: + """Adds a list of OpenFisca Variables to the `TaxBenefitSystem`. See also :any:`add_variable` """ for variable in variables: self.add_variable(variable) - def load_extension(self, extension): - """ - Loads an extension to the tax and benefit system. + def load_extension(self, extension) -> None: + """Loads an extension to the tax and benefit system. :param str extension: The extension to load. Can be an absolute path pointing to an extension directory, or the name of an OpenFisca extension installed as a pip package. @@ -309,12 +312,10 @@ def load_extension(self, extension): message = os.linesep.join( [ traceback.format_exc(), - "Error loading extension: `{}` is neither a directory, nor a package.".format( - extension - ), + f"Error loading extension: `{extension}` is neither a directory, nor a package.", "Are you sure it is installed in your environment? If so, look at the stack trace above to determine the origin of this error.", "See more at .", - ] + ], ) raise ValueError(message) @@ -324,7 +325,7 @@ def load_extension(self, extension): extension_parameters = ParameterNode(directory_path=param_dir) self.parameters.merge(extension_parameters) - def apply_reform(self, reform_path: str) -> "TaxBenefitSystem": + def apply_reform(self, reform_path: str) -> TaxBenefitSystem: """Generates a new tax and benefit system applying a reform to the tax and benefit system. The current tax and benefit system is **not** mutated. @@ -336,8 +337,7 @@ def apply_reform(self, reform_path: str) -> "TaxBenefitSystem": TaxBenefitSystem: A reformed tax and benefit system. Example: - - >>> self.apply_reform('openfisca_france.reforms.inversion_revenus') + >>> self.apply_reform("openfisca_france.reforms.inversion_revenus") """ from openfisca_core.reforms import Reform @@ -345,10 +345,9 @@ def apply_reform(self, reform_path: str) -> "TaxBenefitSystem": try: reform_package, reform_name = reform_path.rsplit(".", 1) except ValueError: + msg = f"`{reform_path}` does not seem to be a path pointing to a reform. A path looks like `some_country_package.reforms.some_reform.`" raise ValueError( - "`{}` does not seem to be a path pointing to a reform. A path looks like `some_country_package.reforms.some_reform.`".format( - reform_path - ) + msg, ) try: reform_module = importlib.import_module(reform_package) @@ -356,19 +355,19 @@ def apply_reform(self, reform_path: str) -> "TaxBenefitSystem": message = os.linesep.join( [ traceback.format_exc(), - "Could not import `{}`.".format(reform_package), + f"Could not import `{reform_package}`.", "Are you sure of this reform module name? If so, look at the stack trace above to determine the origin of this error.", - ] + ], ) raise ValueError(message) reform = getattr(reform_module, reform_name, None) if reform is None: - raise ValueError( - "{} has no attribute {}".format(reform_package, reform_name) - ) + msg = f"{reform_package} has no attribute {reform_name}" + raise ValueError(msg) if not issubclass(reform, Reform): + msg = f"`{reform_path}` does not seem to be a valid Openfisca reform." raise ValueError( - "`{}` does not seem to be a valid Openfisca reform.".format(reform_path) + msg, ) return reform(self) @@ -377,15 +376,14 @@ def get_variable( self, variable_name: str, check_existence: bool = False, - ) -> Optional[Variable]: - """ - Get a variable from the tax and benefit system. + ) -> Variable | None: + """Get a variable from the tax and benefit system. :param variable_name: Name of the requested variable. :param check_existence: If True, raise an error if the requested variable does not exist. """ - variables: Dict[str, Optional[Variable]] = self.variables - variable: Optional[Variable] = variables.get(variable_name) + variables: dict[str, Variable | None] = self.variables + variable: Variable | None = variables.get(variable_name) if isinstance(variable, Variable): return variable @@ -395,25 +393,24 @@ def get_variable( raise VariableNotFoundError(variable_name, self) - def neutralize_variable(self, variable_name: str): - """ - Neutralizes an OpenFisca variable existing in the tax and benefit system. + def neutralize_variable(self, variable_name: str) -> None: + """Neutralizes an OpenFisca variable existing in the tax and benefit system. A neutralized variable always returns its default value when computed. Trying to set inputs for a neutralized variable has no effect except raising a warning. """ self.variables[variable_name] = variables.get_neutralized_variable( - self.get_variable(variable_name) + self.get_variable(variable_name), ) def annualize_variable( self, variable_name: str, - period: Optional[Period] = None, + period: Period | None = None, ) -> None: check: bool - variable: Optional[Variable] + variable: Variable | None annualised_variable: Variable check = bool(period) @@ -426,17 +423,15 @@ def annualize_variable( self.variables[variable_name] = annualised_variable - def load_parameters(self, path_to_yaml_dir): - """ - Loads the legislation parameter for a directory containing YAML parameters files. + def load_parameters(self, path_to_yaml_dir) -> None: + """Loads the legislation parameter for a directory containing YAML parameters files. :param path_to_yaml_dir: Absolute path towards the YAML parameter directory. Example: + >>> self.load_parameters("/path/to/yaml/parameters/dir") - >>> self.load_parameters('/path/to/yaml/parameters/dir') """ - parameters = ParameterNode("", directory_path=path_to_yaml_dir) if self.preprocess_parameters is not None: @@ -450,12 +445,12 @@ def _get_baseline_parameters_at_instant(self, instant): return self.get_parameters_at_instant(instant) return baseline._get_baseline_parameters_at_instant(instant) - @functools.lru_cache() # noqa BO19 + @functools.lru_cache def get_parameters_at_instant( self, - instant: Union[str, int, Period, Instant], - ) -> Optional[ParameterNodeAtInstant]: - """Get the parameters of the legislation at a given instant + instant: str | int | Period | Instant, + ) -> ParameterNodeAtInstant | None: + """Get the parameters of the legislation at a given instant. Args: instant: :obj:`str` formatted "YYYY-MM-DD" or :class:`~openfisca_core.periods.Instant`. @@ -464,8 +459,7 @@ def get_parameters_at_instant( The parameters of the legislation at a given instant. """ - - key: Optional[Instant] + key: Instant | None msg: str if isinstance(instant, Instant): @@ -486,7 +480,7 @@ def get_parameters_at_instant( return self.parameters.get_at_instant(key) - def get_package_metadata(self) -> Dict[str, str]: + def get_package_metadata(self) -> dict[str, str]: """Gets metadata relative to the country package. Returns: @@ -502,7 +496,6 @@ def get_package_metadata(self) -> Dict[str, str]: >>> } """ - # Handle reforms if self.baseline: return self.baseline.get_package_metadata() @@ -515,7 +508,7 @@ def get_package_metadata(self) -> Dict[str, str]: distribution = importlib.metadata.distribution(package_name) source_metadata = distribution.metadata except Exception as e: - log.warn("Unable to load package metadata, exposing default metadata", e) + log.warning("Unable to load package metadata, exposing default metadata", e) source_metadata = { "Name": self.__class__.__name__, "Version": "0.0.0", @@ -526,7 +519,7 @@ def get_package_metadata(self) -> Dict[str, str]: source_file = inspect.getsourcefile(module) location = source_file.split(package_name)[0].rstrip("/") except Exception as e: - log.warn("Unable to load package source folder", e) + log.warning("Unable to load package source folder", e) location = "_unknown_" repository_url = "" @@ -535,7 +528,7 @@ def get_package_metadata(self) -> Dict[str, str]: filter( lambda url: url.startswith("Repository"), source_metadata.get_all("Project-URL"), - ) + ), ).split("Repository, ")[-1] else: # setup.py format repository_url = source_metadata.get("Home-page") @@ -549,8 +542,8 @@ def get_package_metadata(self) -> Dict[str, str]: def get_variables( self, - entity: Optional[Entity] = None, - ) -> Dict[str, Variable]: + entity: Entity | None = None, + ) -> dict[str, Variable]: """Gets all variables contained in a tax and benefit system. Args: @@ -560,16 +553,14 @@ def get_variables( A dictionary, indexed by variable names. """ - if not entity: return self.variables - else: - return { - variable_name: variable - for variable_name, variable in self.variables.items() - # TODO - because entities are copied (see constructor) they can't be compared - if variable.entity.key == entity.key - } + return { + variable_name: variable + for variable_name, variable in self.variables.items() + # TODO - because entities are copied (see constructor) they can't be compared + if variable.entity.key == entity.key + } def clone(self): new = commons.empty_clone(self) diff --git a/openfisca_core/taxscales/abstract_rate_tax_scale.py b/openfisca_core/taxscales/abstract_rate_tax_scale.py index cd04ba872e..84ab4eb913 100644 --- a/openfisca_core/taxscales/abstract_rate_tax_scale.py +++ b/openfisca_core/taxscales/abstract_rate_tax_scale.py @@ -9,18 +9,17 @@ if typing.TYPE_CHECKING: import numpy - NumericalArray = typing.Union[numpy.int_, numpy.float_] + NumericalArray = typing.Union[numpy.int_, numpy.float64] class AbstractRateTaxScale(RateTaxScaleLike): - """ - Base class for various types of rate-based tax scales: marginal rate, + """Base class for various types of rate-based tax scales: marginal rate, linear average rate... """ def __init__( self, - name: typing.Optional[str] = None, + name: str | None = None, option: typing.Any = None, unit: typing.Any = None, ) -> None: @@ -37,6 +36,7 @@ def calc( tax_base: NumericalArray, right: bool, ) -> typing.NoReturn: + msg = "Method 'calc' is not implemented for " f"{self.__class__.__name__}" raise NotImplementedError( - "Method 'calc' is not implemented for " f"{self.__class__.__name__}", + msg, ) diff --git a/openfisca_core/taxscales/abstract_tax_scale.py b/openfisca_core/taxscales/abstract_tax_scale.py index 43b21f8141..933f36d47d 100644 --- a/openfisca_core/taxscales/abstract_tax_scale.py +++ b/openfisca_core/taxscales/abstract_tax_scale.py @@ -9,18 +9,17 @@ if typing.TYPE_CHECKING: import numpy - NumericalArray = typing.Union[numpy.int_, numpy.float_] + NumericalArray = typing.Union[numpy.int_, numpy.float64] class AbstractTaxScale(TaxScaleLike): - """ - Base class for various types of tax scales: amount-based tax scales, + """Base class for various types of tax scales: amount-based tax scales, rate-based tax scales... """ def __init__( self, - name: typing.Optional[str] = None, + name: str | None = None, option: typing.Any = None, unit: numpy.int_ = None, ) -> None: @@ -33,8 +32,9 @@ def __init__( super().__init__(name, option, unit) def __repr__(self) -> typing.NoReturn: + msg = "Method '__repr__' is not implemented for " f"{self.__class__.__name__}" raise NotImplementedError( - "Method '__repr__' is not implemented for " f"{self.__class__.__name__}", + msg, ) def calc( @@ -42,11 +42,13 @@ def calc( tax_base: NumericalArray, right: bool, ) -> typing.NoReturn: + msg = "Method 'calc' is not implemented for " f"{self.__class__.__name__}" raise NotImplementedError( - "Method 'calc' is not implemented for " f"{self.__class__.__name__}", + msg, ) def to_dict(self) -> typing.NoReturn: + msg = f"Method 'to_dict' is not implemented for {self.__class__.__name__}" raise NotImplementedError( - f"Method 'to_dict' is not implemented for " f"{self.__class__.__name__}", + msg, ) diff --git a/openfisca_core/taxscales/amount_tax_scale_like.py b/openfisca_core/taxscales/amount_tax_scale_like.py index 865ce3200c..1dc9acf4b3 100644 --- a/openfisca_core/taxscales/amount_tax_scale_like.py +++ b/openfisca_core/taxscales/amount_tax_scale_like.py @@ -10,12 +10,11 @@ class AmountTaxScaleLike(TaxScaleLike, abc.ABC): - """ - Base class for various types of amount-based tax scales: single amount, + """Base class for various types of amount-based tax scales: single amount, marginal amount... """ - amounts: typing.List + amounts: list def __init__( self, @@ -32,8 +31,8 @@ def __repr__(self) -> str: [ f"- threshold: {threshold}{os.linesep} amount: {amount}" for (threshold, amount) in zip(self.thresholds, self.amounts) - ] - ) + ], + ), ) def add_bracket( diff --git a/openfisca_core/taxscales/helpers.py b/openfisca_core/taxscales/helpers.py index 62ee431be9..687db41a3b 100644 --- a/openfisca_core/taxscales/helpers.py +++ b/openfisca_core/taxscales/helpers.py @@ -18,11 +18,9 @@ def combine_tax_scales( node: ParameterNodeAtInstant, combined_tax_scales: TaxScales = None, ) -> TaxScales: - """ - Combine all the MarginalRateTaxScales in the node into a single + """Combine all the MarginalRateTaxScales in the node into a single MarginalRateTaxScale. """ - name = next(iter(node or []), None) if name is None: diff --git a/openfisca_core/taxscales/linear_average_rate_tax_scale.py b/openfisca_core/taxscales/linear_average_rate_tax_scale.py index 60b2053d5d..ec1b22e0c2 100644 --- a/openfisca_core/taxscales/linear_average_rate_tax_scale.py +++ b/openfisca_core/taxscales/linear_average_rate_tax_scale.py @@ -13,7 +13,7 @@ log = logging.getLogger(__name__) if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float_] + NumericalArray = typing.Union[numpy.int_, numpy.float64] class LinearAverageRateTaxScale(RateTaxScaleLike): @@ -21,7 +21,7 @@ def calc( self, tax_base: NumericalArray, right: bool = False, - ) -> numpy.float_: + ) -> numpy.float64: if len(self.rates) == 1: return tax_base * self.rates[0] diff --git a/openfisca_core/taxscales/marginal_amount_tax_scale.py b/openfisca_core/taxscales/marginal_amount_tax_scale.py index fa8a0897f7..ac021351be 100644 --- a/openfisca_core/taxscales/marginal_amount_tax_scale.py +++ b/openfisca_core/taxscales/marginal_amount_tax_scale.py @@ -7,7 +7,7 @@ from .amount_tax_scale_like import AmountTaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float_] + NumericalArray = typing.Union[numpy.int_, numpy.float64] class MarginalAmountTaxScale(AmountTaxScaleLike): @@ -15,19 +15,20 @@ def calc( self, tax_base: NumericalArray, right: bool = False, - ) -> numpy.float_: - """ - Matches the input amount to a set of brackets and returns the sum of + ) -> numpy.float64: + """Matches the input amount to a set of brackets and returns the sum of cell values from the lowest bracket to the one containing the input. """ base1 = numpy.tile(tax_base, (len(self.thresholds), 1)).T thresholds1 = numpy.tile( - numpy.hstack((self.thresholds, numpy.inf)), (len(tax_base), 1) + numpy.hstack((self.thresholds, numpy.inf)), + (len(tax_base), 1), ) a = numpy.maximum( - numpy.minimum(base1, thresholds1[:, 1:]) - thresholds1[:, :-1], 0 + numpy.minimum(base1, thresholds1[:, 1:]) - thresholds1[:, :-1], + 0, ) return numpy.dot(self.amounts, a.T > 0) diff --git a/openfisca_core/taxscales/marginal_rate_tax_scale.py b/openfisca_core/taxscales/marginal_rate_tax_scale.py index 2604c156e1..c81da8e7e9 100644 --- a/openfisca_core/taxscales/marginal_rate_tax_scale.py +++ b/openfisca_core/taxscales/marginal_rate_tax_scale.py @@ -12,7 +12,7 @@ from .rate_tax_scale_like import RateTaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float_] + NumericalArray = typing.Union[numpy.int_, numpy.float64] class MarginalRateTaxScale(RateTaxScaleLike): @@ -36,10 +36,9 @@ def calc( self, tax_base: NumericalArray, factor: float = 1.0, - round_base_decimals: typing.Optional[int] = None, - ) -> numpy.float_: - """ - Compute the tax amount for the given tax bases by applying a taxscale. + round_base_decimals: int | None = None, + ) -> numpy.float64: + """Compute the tax amount for the given tax bases by applying a taxscale. :param ndarray tax_base: Array of the tax bases. :param float factor: Factor to apply to the thresholds of the taxscale. @@ -68,30 +67,30 @@ def calc( # # numpy.finfo(float_).eps thresholds1 = numpy.outer( - factor + numpy.finfo(numpy.float_).eps, - numpy.array(self.thresholds + [numpy.inf]), + factor + numpy.finfo(numpy.float64).eps, + numpy.array([*self.thresholds, numpy.inf]), ) if round_base_decimals is not None: - thresholds1 = numpy.round_(thresholds1, round_base_decimals) + thresholds1 = numpy.round(thresholds1, round_base_decimals) a = numpy.maximum( - numpy.minimum(base1, thresholds1[:, 1:]) - thresholds1[:, :-1], 0 + numpy.minimum(base1, thresholds1[:, 1:]) - thresholds1[:, :-1], + 0, ) if round_base_decimals is None: return numpy.dot(self.rates, a.T) - else: - r = numpy.tile(self.rates, (len(tax_base), 1)) - b = numpy.round_(a, round_base_decimals) - return numpy.round_(r * b, round_base_decimals).sum(axis=1) + r = numpy.tile(self.rates, (len(tax_base), 1)) + b = numpy.round(a, round_base_decimals) + return numpy.round(r * b, round_base_decimals).sum(axis=1) def combine_bracket( self, - rate: typing.Union[int, float], + rate: int | float, threshold_low: int = 0, - threshold_high: typing.Union[int, bool] = False, + threshold_high: int | bool = False, ) -> None: # Insert threshold_low and threshold_high without modifying rates if threshold_low not in self.thresholds: @@ -119,10 +118,9 @@ def marginal_rates( self, tax_base: NumericalArray, factor: float = 1.0, - round_base_decimals: typing.Optional[int] = None, - ) -> numpy.float_: - """ - Compute the marginal tax rates relevant for the given tax bases. + round_base_decimals: int | None = None, + ) -> numpy.float64: + """Compute the marginal tax rates relevant for the given tax bases. :param ndarray tax_base: Array of the tax bases. :param float factor: Factor to apply to the thresholds of a tax scale. @@ -152,9 +150,8 @@ def marginal_rates( def rate_from_bracket_indice( self, bracket_indice: numpy.int_, - ) -> numpy.float_: - """ - Compute the relevant tax rates for the given bracket indices. + ) -> numpy.float64: + """Compute the relevant tax rates for the given bracket indices. :param: ndarray bracket_indice: Array of the bracket indices. @@ -173,23 +170,24 @@ def rate_from_bracket_indice( >>> tax_scale.rate_from_bracket_indice(bracket_indice) array([0. , 0.25, 0.1 ]) """ - if bracket_indice.max() > len(self.rates) - 1: - raise IndexError( + msg = ( f"bracket_indice parameter ({bracket_indice}) " f"contains one or more bracket indice which is unavailable " f"inside current {self.__class__.__name__} :\n" f"{self}" ) + raise IndexError( + msg, + ) return numpy.array(self.rates)[bracket_indice] def rate_from_tax_base( self, tax_base: NumericalArray, - ) -> numpy.float_: - """ - Compute the relevant tax rates for the given tax bases. + ) -> numpy.float64: + """Compute the relevant tax rates for the given tax bases. :param: ndarray tax_base: Array of the tax bases. @@ -207,12 +205,10 @@ def rate_from_tax_base( >>> tax_scale.rate_from_tax_base(tax_base) array([0.25, 0. , 0.1 ]) """ - return self.rate_from_bracket_indice(self.bracket_indices(tax_base)) def inverse(self) -> MarginalRateTaxScale: - """ - Returns a new instance of MarginalRateTaxScale. + """Returns a new instance of MarginalRateTaxScale. Invert a taxscale: diff --git a/openfisca_core/taxscales/rate_tax_scale_like.py b/openfisca_core/taxscales/rate_tax_scale_like.py index eb8afd872d..60ea9c20e1 100644 --- a/openfisca_core/taxscales/rate_tax_scale_like.py +++ b/openfisca_core/taxscales/rate_tax_scale_like.py @@ -14,20 +14,19 @@ from .tax_scale_like import TaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float_] + NumericalArray = typing.Union[numpy.int_, numpy.float64] class RateTaxScaleLike(TaxScaleLike, abc.ABC): - """ - Base class for various types of rate-based tax scales: marginal rate, + """Base class for various types of rate-based tax scales: marginal rate, linear average rate... """ - rates: typing.List + rates: list def __init__( self, - name: typing.Optional[str] = None, + name: str | None = None, option: typing.Any = None, unit: typing.Any = None, ) -> None: @@ -40,14 +39,14 @@ def __repr__(self) -> str: [ f"- threshold: {threshold}{os.linesep} rate: {rate}" for (threshold, rate) in zip(self.thresholds, self.rates) - ] - ) + ], + ), ) def add_bracket( self, - threshold: typing.Union[int, float], - rate: typing.Union[int, float], + threshold: int | float, + rate: int | float, ) -> None: if threshold in self.thresholds: i = self.thresholds.index(threshold) @@ -62,7 +61,7 @@ def multiply_rates( self, factor: float, inplace: bool = True, - new_name: typing.Optional[str] = None, + new_name: str | None = None, ) -> RateTaxScaleLike: if inplace: assert new_name is None @@ -87,9 +86,9 @@ def multiply_rates( def multiply_thresholds( self, factor: float, - decimals: typing.Optional[int] = None, + decimals: int | None = None, inplace: bool = True, - new_name: typing.Optional[str] = None, + new_name: str | None = None, ) -> RateTaxScaleLike: if inplace: assert new_name is None @@ -128,10 +127,9 @@ def bracket_indices( self, tax_base: NumericalArray, factor: float = 1.0, - round_decimals: typing.Optional[int] = None, + round_decimals: int | None = None, ) -> numpy.int_: - """ - Compute the relevant bracket indices for the given tax bases. + """Compute the relevant bracket indices for the given tax bases. :param ndarray tax_base: Array of the tax bases. :param float factor: Factor to apply to the thresholds. @@ -149,7 +147,6 @@ def bracket_indices( >>> tax_scale.bracket_indices(tax_base) [0, 1] """ - if not numpy.size(numpy.array(self.thresholds)): raise EmptyArgumentError( self.__class__.__name__, @@ -177,11 +174,12 @@ def bracket_indices( # # numpy.finfo(float_).eps thresholds1 = numpy.outer( - +factor + numpy.finfo(numpy.float_).eps, numpy.array(self.thresholds) + +factor + numpy.finfo(numpy.float64).eps, + numpy.array(self.thresholds), ) if round_decimals is not None: - thresholds1 = numpy.round_(thresholds1, round_decimals) + thresholds1 = numpy.round(thresholds1, round_decimals) return (base1 - thresholds1 >= 0).sum(axis=1) - 1 @@ -189,8 +187,7 @@ def threshold_from_tax_base( self, tax_base: NumericalArray, ) -> NumericalArray: - """ - Compute the relevant thresholds for the given tax bases. + """Compute the relevant thresholds for the given tax bases. :param: ndarray tax_base: Array of the tax bases. @@ -209,7 +206,6 @@ def threshold_from_tax_base( >>> tax_scale.threshold_from_tax_base(tax_base) array([200, 500, 0]) """ - return numpy.array(self.thresholds)[self.bracket_indices(tax_base)] def to_dict(self) -> dict: diff --git a/openfisca_core/taxscales/single_amount_tax_scale.py b/openfisca_core/taxscales/single_amount_tax_scale.py index 8f8bdc22c9..1a39396398 100644 --- a/openfisca_core/taxscales/single_amount_tax_scale.py +++ b/openfisca_core/taxscales/single_amount_tax_scale.py @@ -7,7 +7,7 @@ from openfisca_core.taxscales import AmountTaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float_] + NumericalArray = typing.Union[numpy.int_, numpy.float64] class SingleAmountTaxScale(AmountTaxScaleLike): @@ -15,12 +15,11 @@ def calc( self, tax_base: NumericalArray, right: bool = False, - ) -> numpy.float_: - """ - Matches the input amount to a set of brackets and returns the single + ) -> numpy.float64: + """Matches the input amount to a set of brackets and returns the single cell value that fits within that bracket. """ - guarded_thresholds = numpy.array([-numpy.inf] + self.thresholds + [numpy.inf]) + guarded_thresholds = numpy.array([-numpy.inf, *self.thresholds, numpy.inf]) bracket_indices = numpy.digitize( tax_base, @@ -28,6 +27,6 @@ def calc( right=right, ) - guarded_amounts = numpy.array([0] + self.amounts + [0]) + guarded_amounts = numpy.array([0, *self.amounts, 0]) return guarded_amounts[bracket_indices - 1] diff --git a/openfisca_core/taxscales/tax_scale_like.py b/openfisca_core/taxscales/tax_scale_like.py index 2d64e3afeb..683c771127 100644 --- a/openfisca_core/taxscales/tax_scale_like.py +++ b/openfisca_core/taxscales/tax_scale_like.py @@ -5,29 +5,28 @@ import abc import copy -import numpy - from openfisca_core import commons if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float_] + import numpy + + NumericalArray = typing.Union[numpy.int_, numpy.float64] class TaxScaleLike(abc.ABC): - """ - Base class for various types of tax scales: amount-based tax scales, + """Base class for various types of tax scales: amount-based tax scales, rate-based tax scales... """ - name: typing.Optional[str] + name: str | None option: typing.Any unit: typing.Any - thresholds: typing.List + thresholds: list @abc.abstractmethod def __init__( self, - name: typing.Optional[str] = None, + name: str | None = None, option: typing.Any = None, unit: typing.Any = None, ) -> None: @@ -37,30 +36,29 @@ def __init__( self.thresholds = [] def __eq__(self, _other: object) -> typing.NoReturn: + msg = "Method '__eq__' is not implemented for " f"{self.__class__.__name__}" raise NotImplementedError( - "Method '__eq__' is not implemented for " f"{self.__class__.__name__}", + msg, ) def __ne__(self, _other: object) -> typing.NoReturn: + msg = "Method '__ne__' is not implemented for " f"{self.__class__.__name__}" raise NotImplementedError( - "Method '__ne__' is not implemented for " f"{self.__class__.__name__}", + msg, ) @abc.abstractmethod - def __repr__(self) -> str: - ... + def __repr__(self) -> str: ... @abc.abstractmethod def calc( self, tax_base: NumericalArray, right: bool, - ) -> numpy.float_: - ... + ) -> numpy.float64: ... @abc.abstractmethod - def to_dict(self) -> dict: - ... + def to_dict(self) -> dict: ... def copy(self) -> typing.Any: new = commons.empty_clone(self) diff --git a/openfisca_core/tools/__init__.py b/openfisca_core/tools/__init__.py index 9c3b1a4962..1416ed1529 100644 --- a/openfisca_core/tools/__init__.py +++ b/openfisca_core/tools/__init__.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- - - import os import numexpr @@ -15,9 +12,7 @@ def assert_near( message="", relative_error_margin=None, ): - """ - - :param value: Value returned by the test + """:param value: Value returned by the test :param target_value: Value that the test should return to pass :param absolute_error_margin: Absolute error margin authorized :param message: Error message to be displayed if the test fails @@ -26,7 +21,6 @@ def assert_near( Limit : This function cannot be used to assert near periods. """ - import numpy if absolute_error_margin is None and relative_error_margin is None: @@ -48,36 +42,30 @@ def assert_near( if absolute_error_margin is not None: assert ( diff <= absolute_error_margin - ).all(), "{}{} differs from {} with an absolute margin {} > {}".format( - message, value, target_value, diff, absolute_error_margin - ) + ).all(), f"{message}{value} differs from {target_value} with an absolute margin {diff} > {absolute_error_margin}" if relative_error_margin is not None: assert ( diff <= abs(relative_error_margin * target_value) - ).all(), "{}{} differs from {} with a relative margin {} > {}".format( - message, - value, - target_value, - diff, - abs(relative_error_margin * target_value), - ) + ).all(), f"{message}{value} differs from {target_value} with a relative margin {diff} > {abs(relative_error_margin * target_value)}" + return None + return None -def assert_datetime_equals(value, target_value, message=""): - assert (value == target_value).all(), "{}{} differs from {}.".format( - message, value, target_value - ) +def assert_datetime_equals(value, target_value, message="") -> None: + assert ( + value == target_value + ).all(), f"{message}{value} differs from {target_value}." -def assert_enum_equals(value, target_value, message=""): +def assert_enum_equals(value, target_value, message="") -> None: value = value.decode_to_str() - assert (value == target_value).all(), "{}{} differs from {}.".format( - message, value, target_value - ) + assert ( + value == target_value + ).all(), f"{message}{value} differs from {target_value}." def indent(text): - return " {}".format(text.replace(os.linesep, "{} ".format(os.linesep))) + return " {}".format(text.replace(os.linesep, f"{os.linesep} ")) def get_trace_tool_link(scenario, variables, api_url, trace_tool_url): @@ -89,17 +77,16 @@ def get_trace_tool_link(scenario, variables, api_url, trace_tool_url): "scenarios": [scenario_json], "variables": variables, } - url = ( + return ( trace_tool_url + "?" + urllib.urlencode( { "simulation": json.dumps(simulation_json), "api_url": api_url, - } + }, ) ) - return url def eval_expression(expression): diff --git a/openfisca_core/tools/simulation_dumper.py b/openfisca_core/tools/simulation_dumper.py index ab21bd79f0..9b1f5708ad 100644 --- a/openfisca_core/tools/simulation_dumper.py +++ b/openfisca_core/tools/simulation_dumper.py @@ -1,19 +1,14 @@ -# -*- coding: utf-8 -*- - - import os -import numpy as np +import numpy from openfisca_core.data_storage import OnDiskStorage from openfisca_core.periods import DateUnit from openfisca_core.simulations import Simulation -def dump_simulation(simulation, directory): - """ - Write simulation data to directory, so that it can be restored later. - """ +def dump_simulation(simulation, directory) -> None: + """Write simulation data to directory, so that it can be restored later.""" parent_directory = os.path.abspath(os.path.join(directory, os.pardir)) if not os.path.isdir(parent_directory): # To deal with reforms os.mkdir(parent_directory) @@ -21,7 +16,8 @@ def dump_simulation(simulation, directory): os.mkdir(directory) if os.listdir(directory): - raise ValueError("Directory '{}' is not empty".format(directory)) + msg = f"Directory '{directory}' is not empty" + raise ValueError(msg) entities_dump_dir = os.path.join(directory, "__entities__") os.mkdir(entities_dump_dir) @@ -36,11 +32,10 @@ def dump_simulation(simulation, directory): def restore_simulation(directory, tax_benefit_system, **kwargs): - """ - Restore simulation from directory - """ + """Restore simulation from directory.""" simulation = Simulation( - tax_benefit_system, tax_benefit_system.instantiate_entities() + tax_benefit_system, + tax_benefit_system.instantiate_entities(), ) entities_dump_dir = os.path.join(directory, "__entities__") @@ -64,68 +59,74 @@ def restore_simulation(directory, tax_benefit_system, **kwargs): return simulation -def _dump_holder(holder, directory): +def _dump_holder(holder, directory) -> None: disk_storage = holder.create_disk_storage(directory, preserve=True) for period in holder.get_known_periods(): value = holder.get_array(period) disk_storage.put(value, period) -def _dump_entity(population, directory): +def _dump_entity(population, directory) -> None: path = os.path.join(directory, population.entity.key) os.mkdir(path) - np.save(os.path.join(path, "id.npy"), population.ids) + numpy.save(os.path.join(path, "id.npy"), population.ids) if population.entity.is_person: return - np.save(os.path.join(path, "members_position.npy"), population.members_position) - np.save(os.path.join(path, "members_entity_id.npy"), population.members_entity_id) + numpy.save(os.path.join(path, "members_position.npy"), population.members_position) + numpy.save( + os.path.join(path, "members_entity_id.npy"), population.members_entity_id + ) flattened_roles = population.entity.flattened_roles if len(flattened_roles) == 0: - encoded_roles = np.int64(0) + encoded_roles = numpy.int64(0) else: - encoded_roles = np.select( + encoded_roles = numpy.select( [population.members_role == role for role in flattened_roles], [role.key for role in flattened_roles], ) - np.save(os.path.join(path, "members_role.npy"), encoded_roles) + numpy.save(os.path.join(path, "members_role.npy"), encoded_roles) def _restore_entity(population, directory): path = os.path.join(directory, population.entity.key) - population.ids = np.load(os.path.join(path, "id.npy")) + population.ids = numpy.load(os.path.join(path, "id.npy")) if population.entity.is_person: - return + return None - population.members_position = np.load(os.path.join(path, "members_position.npy")) - population.members_entity_id = np.load(os.path.join(path, "members_entity_id.npy")) - encoded_roles = np.load(os.path.join(path, "members_role.npy")) + population.members_position = numpy.load(os.path.join(path, "members_position.npy")) + population.members_entity_id = numpy.load( + os.path.join(path, "members_entity_id.npy") + ) + encoded_roles = numpy.load(os.path.join(path, "members_role.npy")) flattened_roles = population.entity.flattened_roles if len(flattened_roles) == 0: - population.members_role = np.int64(0) + population.members_role = numpy.int64(0) else: - population.members_role = np.select( + population.members_role = numpy.select( [encoded_roles == role.key for role in flattened_roles], - [role for role in flattened_roles], + list(flattened_roles), ) person_count = len(population.members_entity_id) population.count = max(population.members_entity_id) + 1 return person_count -def _restore_holder(simulation, variable, directory): +def _restore_holder(simulation, variable, directory) -> None: storage_dir = os.path.join(directory, variable) is_variable_eternal = ( simulation.tax_benefit_system.get_variable(variable).definition_period == DateUnit.ETERNITY ) disk_storage = OnDiskStorage( - storage_dir, is_eternal=is_variable_eternal, preserve_storage_dir=True + storage_dir, + is_eternal=is_variable_eternal, + preserve_storage_dir=True, ) disk_storage.restore() diff --git a/openfisca_core/tools/test_runner.py b/openfisca_core/tools/test_runner.py index ea77401c6e..34e2d0a210 100644 --- a/openfisca_core/tools/test_runner.py +++ b/openfisca_core/tools/test_runner.py @@ -1,10 +1,8 @@ from __future__ import annotations -from typing import Any, Dict, Optional, Sequence, Union +from typing import TYPE_CHECKING, Any from typing_extensions import Literal, TypedDict -from openfisca_core.types import TaxBenefitSystem - import dataclasses import os import pathlib @@ -20,13 +18,18 @@ from openfisca_core.tools import assert_near from openfisca_core.warnings import LibYAMLWarning +if TYPE_CHECKING: + from collections.abc import Sequence + + from openfisca_core.types import TaxBenefitSystem + class Options(TypedDict, total=False): aggregate: bool - ignore_variables: Optional[Sequence[str]] - max_depth: Optional[int] - name_filter: Optional[str] - only_variables: Optional[Sequence[str]] + ignore_variables: Sequence[str] | None + max_depth: int | None + name_filter: str | None + only_variables: Sequence[str] | None pdb: bool performance_graph: bool performance_tables: bool @@ -35,9 +38,9 @@ class Options(TypedDict, total=False): @dataclasses.dataclass(frozen=True) class ErrorMargin: - __root__: Dict[Union[str, Literal["default"]], Optional[float]] + __root__: dict[str | Literal["default"], float | None] - def __getitem__(self, key: str) -> Optional[float]: + def __getitem__(self, key: str) -> float | None: if key in self.__root__: return self.__root__[key] @@ -49,19 +52,17 @@ class Test: absolute_error_margin: ErrorMargin relative_error_margin: ErrorMargin name: str = "" - input: Dict[str, Union[float, Dict[str, float]]] = dataclasses.field( - default_factory=dict - ) - output: Optional[Dict[str, Union[float, Dict[str, float]]]] = None - period: Optional[str] = None + input: dict[str, float | dict[str, float]] = dataclasses.field(default_factory=dict) + output: dict[str, float | dict[str, float]] | None = None + period: str | None = None reforms: Sequence[str] = dataclasses.field(default_factory=list) - keywords: Optional[Sequence[str]] = None + keywords: Sequence[str] | None = None extensions: Sequence[str] = dataclasses.field(default_factory=list) - description: Optional[str] = None - max_spiral_loops: Optional[int] = None + description: str | None = None + max_spiral_loops: int | None = None -def build_test(params: Dict[str, Any]) -> Test: +def build_test(params: dict[str, Any]) -> Test: for key in ["absolute_error_margin", "relative_error_margin"]: value = params.get(key) @@ -111,14 +112,14 @@ def import_yaml(): yaml, Loader = import_yaml() -_tax_benefit_system_cache: Dict = {} +_tax_benefit_system_cache: dict = {} options: Options = Options() def run_tests( tax_benefit_system: TaxBenefitSystem, - paths: Union[str, Sequence[str]], + paths: str | Sequence[str], options: Options = options, ) -> int: """Runs all the YAML tests contained in a file or a directory. @@ -147,7 +148,6 @@ def run_tests( +-------------------------------+-----------+-------------------------------------------+ """ - argv = [] plugins = [OpenFiscaPlugin(tax_benefit_system, options)] @@ -164,8 +164,8 @@ def run_tests( class YamlFile(pytest.File): - def __init__(self, *, tax_benefit_system, options, **kwargs): - super(YamlFile, self).__init__(**kwargs) + def __init__(self, *, tax_benefit_system, options, **kwargs) -> None: + super().__init__(**kwargs) self.tax_benefit_system = tax_benefit_system self.options = options @@ -177,12 +177,12 @@ def collect(self): [ traceback.format_exc(), f"'{self.path}' is not a valid YAML file. Check the stack trace above for more details.", - ] + ], ) raise ValueError(message) if not isinstance(tests, list): - tests: Sequence[Dict] = [tests] + tests: Sequence[dict] = [tests] for test in tests: if not self.should_ignore(test): @@ -205,19 +205,17 @@ def should_ignore(self, test): class YamlItem(pytest.Item): - """ - Terminal nodes of the test collection tree. - """ + """Terminal nodes of the test collection tree.""" - def __init__(self, *, baseline_tax_benefit_system, test, options, **kwargs): - super(YamlItem, self).__init__(**kwargs) + def __init__(self, *, baseline_tax_benefit_system, test, options, **kwargs) -> None: + super().__init__(**kwargs) self.baseline_tax_benefit_system = baseline_tax_benefit_system self.options = options self.test = build_test(test) self.simulation = None self.tax_benefit_system = None - def runtest(self): + def runtest(self) -> None: self.name = self.test.name if self.test.output is None: @@ -247,10 +245,10 @@ def runtest(self): raise except Exception as e: error_message = os.linesep.join( - [str(e), "", f"Unexpected error raised while parsing '{self.path}'"] + [str(e), "", f"Unexpected error raised while parsing '{self.path}'"], ) raise ValueError(error_message).with_traceback( - sys.exc_info()[2] + sys.exc_info()[2], ) from e # Keep the stack trace from the root error if max_spiral_loops: @@ -268,17 +266,16 @@ def runtest(self): if performance_tables: self.generate_performance_tables(tracer) - def print_computation_log(self, tracer, aggregate, max_depth): - print("Computation log:") # noqa T001 + def print_computation_log(self, tracer, aggregate, max_depth) -> None: tracer.print_computation_log(aggregate, max_depth) - def generate_performance_graph(self, tracer): + def generate_performance_graph(self, tracer) -> None: tracer.generate_performance_graph(".") - def generate_performance_tables(self, tracer): + def generate_performance_tables(self, tracer) -> None: tracer.generate_performance_tables(".") - def check_output(self): + def check_output(self) -> None: output = self.test.output if output is None: @@ -296,18 +293,25 @@ def check_output(self): for variable_name, value in instance_values.items(): entity_index = population.get_index(instance_id) self.check_variable( - variable_name, value, self.test.period, entity_index + variable_name, + value, + self.test.period, + entity_index, ) else: raise VariableNotFound(key, self.tax_benefit_system) def check_variable( - self, variable_name: str, expected_value, period, entity_index=None + self, + variable_name: str, + expected_value, + period, + entity_index=None, ): if self.should_ignore_variable(variable_name): - return + return None - if isinstance(expected_value, Dict): + if isinstance(expected_value, dict): for requested_period, expected_value_at_period in expected_value.items(): self.check_variable( variable_name, @@ -345,9 +349,10 @@ def should_ignore_variable(self, variable_name: str): def repr_failure(self, excinfo): if not isinstance( - excinfo.value, (AssertionError, VariableNotFound, SituationParsingError) + excinfo.value, + (AssertionError, VariableNotFound, SituationParsingError), ): - return super(YamlItem, self).repr_failure(excinfo) + return super().repr_failure(excinfo) message = excinfo.value.args[0] if isinstance(excinfo.value, SituationParsingError): @@ -355,21 +360,20 @@ def repr_failure(self, excinfo): return os.linesep.join( [ - f"{str(self.path)}:", - f" Test '{str(self.name)}':", + f"{self.path!s}:", + f" Test '{self.name!s}':", textwrap.indent(message, " "), - ] + ], ) -class OpenFiscaPlugin(object): - def __init__(self, tax_benefit_system, options): +class OpenFiscaPlugin: + def __init__(self, tax_benefit_system, options) -> None: self.tax_benefit_system = tax_benefit_system self.options = options def pytest_collect_file(self, parent, path): - """ - Called by pytest for all plugins. + """Called by pytest for all plugins. :return: The collector for test methods. """ if path.ext in [".yaml", ".yml"]: @@ -379,6 +383,7 @@ def pytest_collect_file(self, parent, path): tax_benefit_system=self.tax_benefit_system, options=self.options, ) + return None def _get_tax_benefit_system(baseline, reforms, extensions): @@ -396,7 +401,7 @@ def _get_tax_benefit_system(baseline, reforms, extensions): for reform_path in reforms: current_tax_benefit_system = current_tax_benefit_system.apply_reform( - reform_path + reform_path, ) for extension in extensions: diff --git a/openfisca_core/tracers/computation_log.py b/openfisca_core/tracers/computation_log.py index 1013b828f2..6310eb8849 100644 --- a/openfisca_core/tracers/computation_log.py +++ b/openfisca_core/tracers/computation_log.py @@ -1,17 +1,17 @@ from __future__ import annotations import typing -from typing import List, Optional, Union +from typing import Union import numpy from openfisca_core.indexed_enums import EnumArray -from .. import tracers - if typing.TYPE_CHECKING: from numpy.typing import ArrayLike + from openfisca_core import tracers + Array = Union[EnumArray, ArrayLike] @@ -23,7 +23,7 @@ def __init__(self, full_tracer: tracers.FullTracer) -> None: def display( self, - value: Optional[Array], + value: Array | None, ) -> str: if isinstance(value, EnumArray): value = value.decode_to_str() @@ -33,8 +33,8 @@ def display( def lines( self, aggregate: bool = False, - max_depth: Optional[int] = None, - ) -> List[str]: + max_depth: int | None = None, + ) -> list[str]: depth = 1 lines_by_tree = [ @@ -45,8 +45,7 @@ def lines( return self._flatten(lines_by_tree) def print_log(self, aggregate=False, max_depth=None) -> None: - """ - Print the computation log of a simulation. + """Print the computation log of a simulation. If ``aggregate`` is ``False`` (default), print the value of each computed vector. @@ -61,16 +60,16 @@ def print_log(self, aggregate=False, max_depth=None) -> None: If ``max_depth`` is set, for example to ``3``, only print computed vectors up to a depth of ``max_depth``. """ - for line in self.lines(aggregate, max_depth): - print(line) # noqa T001 + for _line in self.lines(aggregate, max_depth): + pass def _get_node_log( self, node: tracers.TraceNode, depth: int, aggregate: bool, - max_depth: Optional[int], - ) -> List[str]: + max_depth: int | None, + ) -> list[str]: if max_depth is not None and depth > max_depth: return [] @@ -88,7 +87,7 @@ def _print_line( depth: int, node: tracers.TraceNode, aggregate: bool, - max_depth: Optional[int], + max_depth: int | None, ) -> str: indent = " " * depth value = node.value @@ -103,7 +102,7 @@ def _print_line( "avg": numpy.mean(value), "max": numpy.max(value), "min": numpy.min(value), - } + }, ) except TypeError: @@ -116,6 +115,6 @@ def _print_line( def _flatten( self, - lists: List[List[str]], - ) -> List[str]: + lists: list[list[str]], + ) -> list[str]: return [item for list_ in lists for item in list_] diff --git a/openfisca_core/tracers/flat_trace.py b/openfisca_core/tracers/flat_trace.py index 25aa75f21d..2090d537b8 100644 --- a/openfisca_core/tracers/flat_trace.py +++ b/openfisca_core/tracers/flat_trace.py @@ -1,18 +1,19 @@ from __future__ import annotations import typing -from typing import Dict, Optional, Union +from typing import Union import numpy -from openfisca_core import tracers from openfisca_core.indexed_enums import EnumArray if typing.TYPE_CHECKING: from numpy.typing import ArrayLike + from openfisca_core import tracers + Array = Union[EnumArray, ArrayLike] - Trace = Dict[str, dict] + Trace = dict[str, dict] class FlatTrace: @@ -39,7 +40,7 @@ def get_trace(self) -> dict: key: node_trace for key, node_trace in self._get_flat_trace(node).items() if key not in trace - } + }, ) return trace @@ -52,13 +53,14 @@ def get_serialized_trace(self) -> dict: def serialize( self, - value: Optional[Array], - ) -> Union[Optional[Array], list]: + value: Array | None, + ) -> Array | None | list: if isinstance(value, EnumArray): value = value.decode_to_str() if isinstance(value, numpy.ndarray) and numpy.issubdtype( - value.dtype, numpy.dtype(bytes) + value.dtype, + numpy.dtype(bytes), ): value = value.astype(numpy.dtype(str)) @@ -73,7 +75,7 @@ def _get_flat_trace( ) -> Trace: key = self.key(node) - node_trace = { + return { key: { "dependencies": [self.key(child) for child in node.children], "parameters": { @@ -85,5 +87,3 @@ def _get_flat_trace( "formula_time": node.formula_time(), }, } - - return node_trace diff --git a/openfisca_core/tracers/full_tracer.py b/openfisca_core/tracers/full_tracer.py index 085607e125..9fa94d5ab5 100644 --- a/openfisca_core/tracers/full_tracer.py +++ b/openfisca_core/tracers/full_tracer.py @@ -1,24 +1,25 @@ from __future__ import annotations import typing -from typing import Dict, Iterator, List, Optional, Union +from typing import Union import time -from .. import tracers +from openfisca_core import tracers if typing.TYPE_CHECKING: + from collections.abc import Iterator from numpy.typing import ArrayLike from openfisca_core.periods import Period - Stack = List[Dict[str, Union[str, Period]]] + Stack = list[dict[str, Union[str, Period]]] class FullTracer: _simple_tracer: tracers.SimpleTracer _trees: list - _current_node: Optional[tracers.TraceNode] + _current_node: tracers.TraceNode | None def __init__(self) -> None: self._simple_tracer = tracers.SimpleTracer() @@ -66,7 +67,7 @@ def record_parameter_access( def _record_start_time( self, - time_in_s: Optional[float] = None, + time_in_s: float | None = None, ) -> None: if time_in_s is None: time_in_s = self._get_time_in_sec() @@ -85,7 +86,7 @@ def record_calculation_end(self) -> None: def _record_end_time( self, - time_in_s: Optional[float] = None, + time_in_s: float | None = None, ) -> None: if time_in_s is None: time_in_s = self._get_time_in_sec() @@ -102,7 +103,7 @@ def stack(self) -> Stack: return self._simple_tracer.stack @property - def trees(self) -> List[tracers.TraceNode]: + def trees(self) -> list[tracers.TraceNode]: return self._trees @property @@ -120,7 +121,7 @@ def flat_trace(self) -> tracers.FlatTrace: def _get_time_in_sec(self) -> float: return time.time_ns() / (10**9) - def print_computation_log(self, aggregate=False, max_depth=None): + def print_computation_log(self, aggregate=False, max_depth=None) -> None: self.computation_log.print_log(aggregate, max_depth) def generate_performance_graph(self, dir_path: str) -> None: diff --git a/openfisca_core/tracers/performance_log.py b/openfisca_core/tracers/performance_log.py index 89917dc50e..f69a3dd3a2 100644 --- a/openfisca_core/tracers/performance_log.py +++ b/openfisca_core/tracers/performance_log.py @@ -8,12 +8,12 @@ import json import os -from .. import tracers +from openfisca_core import tracers if typing.TYPE_CHECKING: - Trace = typing.Dict[str, dict] - Calculation = typing.Tuple[str, dict] - SortedTrace = typing.List[Calculation] + Trace = dict[str, dict] + Calculation = tuple[str, dict] + SortedTrace = list[Calculation] class PerformanceLog: @@ -54,7 +54,7 @@ def generate_performance_tables(self, dir_path: str) -> None: aggregated_csv_rows = [ {"name": key, **aggregated_time} for key, aggregated_time in self.aggregate_calculation_times( - flat_trace + flat_trace, ).items() ] @@ -66,7 +66,7 @@ def generate_performance_tables(self, dir_path: str) -> None: def aggregate_calculation_times( self, flat_trace: Trace, - ) -> typing.Dict[str, dict]: + ) -> dict[str, dict]: def _aggregate_calculations(calculations: list) -> dict: calculation_count = len(calculations) @@ -83,10 +83,10 @@ def _aggregate_calculations(calculations: list) -> dict: "calculation_time": tracers.TraceNode.round(calculation_time), "formula_time": tracers.TraceNode.round(formula_time), "avg_calculation_time": tracers.TraceNode.round( - calculation_time / calculation_count + calculation_time / calculation_count, ), "avg_formula_time": tracers.TraceNode.round( - formula_time / calculation_count + formula_time / calculation_count, ), } @@ -98,7 +98,8 @@ def _groupby(calculation: Calculation) -> str: return { variable_name: _aggregate_calculations(list(calculations)) for variable_name, calculations in itertools.groupby( - all_calculations, _groupby + all_calculations, + _groupby, ) } @@ -122,7 +123,7 @@ def _json_tree(self, tree: tracers.TraceNode) -> dict: "children": children, } - def _write_csv(self, path: str, rows: typing.List[dict]) -> None: + def _write_csv(self, path: str, rows: list[dict]) -> None: fieldnames = list(rows[0].keys()) with open(path, "w") as csv_file: diff --git a/openfisca_core/tracers/simple_tracer.py b/openfisca_core/tracers/simple_tracer.py index 27bcad2e8c..84328730ef 100644 --- a/openfisca_core/tracers/simple_tracer.py +++ b/openfisca_core/tracers/simple_tracer.py @@ -1,14 +1,14 @@ from __future__ import annotations import typing -from typing import Dict, List, Union +from typing import Union if typing.TYPE_CHECKING: from numpy.typing import ArrayLike from openfisca_core.periods import Period - Stack = List[Dict[str, Union[str, Period]]] + Stack = list[dict[str, Union[str, Period]]] class SimpleTracer: @@ -23,7 +23,7 @@ def record_calculation_start(self, variable: str, period: Period | int) -> None: def record_calculation_result(self, value: ArrayLike) -> None: pass # ignore calculation result - def record_parameter_access(self, parameter: str, period, value): + def record_parameter_access(self, parameter: str, period, value) -> None: pass def record_calculation_end(self) -> None: diff --git a/openfisca_core/tracers/trace_node.py b/openfisca_core/tracers/trace_node.py index 4e0cceae0a..ff55a5714f 100644 --- a/openfisca_core/tracers/trace_node.py +++ b/openfisca_core/tracers/trace_node.py @@ -18,10 +18,10 @@ class TraceNode: name: str period: Period - parent: typing.Optional[TraceNode] = None - children: typing.List[TraceNode] = dataclasses.field(default_factory=list) - parameters: typing.List[TraceNode] = dataclasses.field(default_factory=list) - value: typing.Optional[Array] = None + parent: TraceNode | None = None + children: list[TraceNode] = dataclasses.field(default_factory=list) + parameters: list[TraceNode] = dataclasses.field(default_factory=list) + value: Array | None = None start: float = 0 end: float = 0 diff --git a/openfisca_core/tracers/tracing_parameter_node_at_instant.py b/openfisca_core/tracers/tracing_parameter_node_at_instant.py index f618f59e97..074c24221d 100644 --- a/openfisca_core/tracers/tracing_parameter_node_at_instant.py +++ b/openfisca_core/tracers/tracing_parameter_node_at_instant.py @@ -7,8 +7,6 @@ from openfisca_core import parameters -from .. import tracers - ParameterNode = Union[ parameters.VectorialParameterNodeAtInstant, parameters.ParameterNodeAtInstant, @@ -17,6 +15,8 @@ if typing.TYPE_CHECKING: from numpy.typing import ArrayLike + from openfisca_core import tracers + Child = Union[ParameterNode, ArrayLike] @@ -32,7 +32,7 @@ def __init__( def __getattr__( self, key: str, - ) -> Union[TracingParameterNodeAtInstant, Child]: + ) -> TracingParameterNodeAtInstant | Child: child = getattr(self.parameter_node_at_instant, key) return self.get_traced_child(child, key) @@ -44,16 +44,16 @@ def __iter__(self): def __getitem__( self, - key: Union[str, ArrayLike], - ) -> Union[TracingParameterNodeAtInstant, Child]: + key: str | ArrayLike, + ) -> TracingParameterNodeAtInstant | Child: child = self.parameter_node_at_instant[key] return self.get_traced_child(child, key) def get_traced_child( self, child: Child, - key: Union[str, ArrayLike], - ) -> Union[TracingParameterNodeAtInstant, Child]: + key: str | ArrayLike, + ) -> TracingParameterNodeAtInstant | Child: period = self.parameter_node_at_instant._instant_str if isinstance( @@ -75,9 +75,9 @@ def get_traced_child( name = self.parameter_node_at_instant._name else: - name = ".".join([self.parameter_node_at_instant._name, key]) + name = f"{self.parameter_node_at_instant._name}.{key}" - if isinstance(child, (numpy.ndarray,) + parameters.ALLOWED_PARAM_TYPES): + if isinstance(child, (numpy.ndarray, *parameters.ALLOWED_PARAM_TYPES)): self.tracer.record_parameter_access(name, period, child) return child diff --git a/openfisca_core/types.py b/openfisca_core/types.py index b34a555434..16a1f0e90e 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -35,28 +35,23 @@ class CoreEntity(Protocol): plural: Any @abc.abstractmethod - def check_role_validity(self, role: Any) -> None: - ... + def check_role_validity(self, role: Any) -> None: ... @abc.abstractmethod - def check_variable_defined_for_entity(self, variable_name: Any) -> None: - ... + def check_variable_defined_for_entity(self, variable_name: Any) -> None: ... @abc.abstractmethod def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Any | None: - ... + ) -> Any | None: ... -class SingleEntity(CoreEntity, Protocol): - ... +class SingleEntity(CoreEntity, Protocol): ... -class GroupEntity(CoreEntity, Protocol): - ... +class GroupEntity(CoreEntity, Protocol): ... class Role(Protocol): @@ -65,8 +60,7 @@ class Role(Protocol): subroles: Any @property - def key(self) -> str: - ... + def key(self) -> str: ... # Holders @@ -74,40 +68,34 @@ def key(self) -> str: class Holder(Protocol): @abc.abstractmethod - def clone(self, population: Any) -> Holder: - ... + def clone(self, population: Any) -> Holder: ... @abc.abstractmethod - def get_memory_usage(self) -> Any: - ... + def get_memory_usage(self) -> Any: ... # Parameters @typing_extensions.runtime_checkable -class ParameterNodeAtInstant(Protocol): - ... +class ParameterNodeAtInstant(Protocol): ... # Periods -class Instant(Protocol): - ... +class Instant(Protocol): ... @typing_extensions.runtime_checkable class Period(Protocol): @property @abc.abstractmethod - def start(self) -> Any: - ... + def start(self) -> Any: ... @property @abc.abstractmethod - def unit(self) -> Any: - ... + def unit(self) -> Any: ... # Populations @@ -117,8 +105,7 @@ class Population(Protocol): entity: Any @abc.abstractmethod - def get_holder(self, variable_name: Any) -> Any: - ... + def get_holder(self, variable_name: Any) -> Any: ... # Simulations @@ -126,20 +113,16 @@ def get_holder(self, variable_name: Any) -> Any: class Simulation(Protocol): @abc.abstractmethod - def calculate(self, variable_name: Any, period: Any) -> Any: - ... + def calculate(self, variable_name: Any, period: Any) -> Any: ... @abc.abstractmethod - def calculate_add(self, variable_name: Any, period: Any) -> Any: - ... + def calculate_add(self, variable_name: Any, period: Any) -> Any: ... @abc.abstractmethod - def calculate_divide(self, variable_name: Any, period: Any) -> Any: - ... + def calculate_divide(self, variable_name: Any, period: Any) -> Any: ... @abc.abstractmethod - def get_population(self, plural: Any | None) -> Any: - ... + def get_population(self, plural: Any | None) -> Any: ... # Tax-Benefit systems @@ -171,11 +154,9 @@ def __call__( population: Population, instant: Instant, params: Params, - ) -> Array[Any]: - ... + ) -> Array[Any]: ... class Params(Protocol): @abc.abstractmethod - def __call__(self, instant: Instant) -> ParameterNodeAtInstant: - ... + def __call__(self, instant: Instant) -> ParameterNodeAtInstant: ... diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index ce1eede9fc..a9cd5df801 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -1,19 +1,19 @@ from __future__ import annotations -from typing import Optional +from typing import TYPE_CHECKING import sortedcontainers -from openfisca_core.periods import Period - -from .. import variables +if TYPE_CHECKING: + from openfisca_core import variables + from openfisca_core.periods import Period def get_annualized_variable( - variable: variables.Variable, annualization_period: Optional[Period] = None + variable: variables.Variable, + annualization_period: Period | None = None, ) -> variables.Variable: - """ - Returns a clone of ``variable`` that is annualized for the period ``annualization_period``. + """Returns a clone of ``variable`` that is annualized for the period ``annualization_period``. When annualized, a variable's formula is only called for a January calculation, and the results for other months are assumed to be identical. """ @@ -34,23 +34,24 @@ def annual_formula(population, period, parameters): { key: make_annual_formula(formula, annualization_period) for key, formula in variable.formulas.items() - } + }, ) return new_variable def get_neutralized_variable(variable): - """ - Return a new neutralized variable (to be used by reforms). + """Return a new neutralized variable (to be used by reforms). A neutralized variable always returns its default value, and does not cache anything. """ result = variable.clone() result.is_neutralized = True result.label = ( - "[Neutralized]" - if variable.label is None - else "[Neutralized] {}".format(variable.label), + ( + "[Neutralized]" + if variable.label is None + else f"[Neutralized] {variable.label}" + ), ) return result diff --git a/openfisca_core/variables/tests/test_definition_period.py b/openfisca_core/variables/tests/test_definition_period.py index 7938aaeaef..8ef9bfaa87 100644 --- a/openfisca_core/variables/tests/test_definition_period.py +++ b/openfisca_core/variables/tests/test_definition_period.py @@ -13,31 +13,31 @@ class TestVariable(Variable): return TestVariable -def test_weekday_variable(variable): +def test_weekday_variable(variable) -> None: variable.definition_period = periods.WEEKDAY assert variable() -def test_week_variable(variable): +def test_week_variable(variable) -> None: variable.definition_period = periods.WEEK assert variable() -def test_day_variable(variable): +def test_day_variable(variable) -> None: variable.definition_period = periods.DAY assert variable() -def test_month_variable(variable): +def test_month_variable(variable) -> None: variable.definition_period = periods.MONTH assert variable() -def test_year_variable(variable): +def test_year_variable(variable) -> None: variable.definition_period = periods.YEAR assert variable() -def test_eternity_variable(variable): +def test_eternity_variable(variable) -> None: variable.definition_period = periods.ETERNITY assert variable() diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index e70c0d05d9..ac226dda2f 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -1,8 +1,6 @@ from __future__ import annotations -from typing import Optional, Union - -from openfisca_core.types import Formula, Instant +from typing import TYPE_CHECKING, NoReturn import datetime import re @@ -18,10 +16,12 @@ from . import config, helpers +if TYPE_CHECKING: + from openfisca_core.types import Formula, Instant + class Variable: - """ - A `variable `_ of the legislation. + """A `variable `_ of the legislation. Main attributes: @@ -102,7 +102,7 @@ class Variable: __name__: str - def __init__(self, baseline_variable=None): + def __init__(self, baseline_variable=None) -> None: self.name = self.__class__.__name__ attr = { name: value @@ -111,21 +111,30 @@ def __init__(self, baseline_variable=None): } self.baseline_variable = baseline_variable self.value_type = self.set( - attr, "value_type", required=True, allowed_values=config.VALUE_TYPES.keys() + attr, + "value_type", + required=True, + allowed_values=config.VALUE_TYPES.keys(), ) self.dtype = config.VALUE_TYPES[self.value_type]["dtype"] self.json_type = config.VALUE_TYPES[self.value_type]["json_type"] if self.value_type == Enum: self.possible_values = self.set( - attr, "possible_values", required=True, setter=self.set_possible_values + attr, + "possible_values", + required=True, + setter=self.set_possible_values, ) if self.value_type == str: self.max_length = self.set(attr, "max_length", allowed_type=int) if self.max_length: - self.dtype = "|S{}".format(self.max_length) + self.dtype = f"|S{self.max_length}" if self.value_type == Enum: self.default_value = self.set( - attr, "default_value", allowed_type=self.possible_values, required=True + attr, + "default_value", + allowed_type=self.possible_values, + required=True, ) else: self.default_value = self.set( @@ -136,7 +145,10 @@ def __init__(self, baseline_variable=None): ) self.entity = self.set(attr, "entity", required=True, setter=self.set_entity) self.definition_period = self.set( - attr, "definition_period", required=True, allowed_values=DateUnit + attr, + "definition_period", + required=True, + allowed_values=DateUnit, ) self.label = self.set(attr, "label", allowed_type=str, setter=self.set_label) self.end = self.set(attr, "end", allowed_type=str, setter=self.set_end) @@ -144,11 +156,14 @@ def __init__(self, baseline_variable=None): self.cerfa_field = self.set(attr, "cerfa_field", allowed_type=(str, dict)) self.unit = self.set(attr, "unit", allowed_type=str) self.documentation = self.set( - attr, "documentation", allowed_type=str, setter=self.set_documentation + attr, + "documentation", + allowed_type=str, + setter=self.set_documentation, ) self.set_input = self.set_set_input(attr.pop("set_input", None)) self.calculate_output = self.set_calculate_output( - attr.pop("calculate_output", None) + attr.pop("calculate_output", None), ) self.is_period_size_independent = self.set( attr, @@ -163,15 +178,18 @@ def __init__(self, baseline_variable=None): ) formulas_attr, unexpected_attrs = helpers._partition( - attr, lambda name, value: name.startswith(config.FORMULA_NAME_PREFIX) + attr, + lambda name, value: name.startswith(config.FORMULA_NAME_PREFIX), ) self.formulas = self.set_formulas(formulas_attr) if unexpected_attrs: + msg = 'Unexpected attributes in definition of variable "{}": {!r}'.format( + self.name, + ", ".join(sorted(unexpected_attrs.keys())), + ) raise ValueError( - 'Unexpected attributes in definition of variable "{}": {!r}'.format( - self.name, ", ".join(sorted(unexpected_attrs.keys())) - ) + msg, ) self.is_neutralized = False @@ -192,16 +210,14 @@ def set( if value is None and self.baseline_variable: return getattr(self.baseline_variable, attribute_name) if required and value is None: + msg = f"Missing attribute '{attribute_name}' in definition of variable '{self.name}'." raise ValueError( - "Missing attribute '{}' in definition of variable '{}'.".format( - attribute_name, self.name - ) + msg, ) if allowed_values is not None and value not in allowed_values: + msg = f"Invalid value '{value}' for attribute '{attribute_name}' in variable '{self.name}'. Allowed values are '{allowed_values}'." raise ValueError( - "Invalid value '{}' for attribute '{}' in variable '{}'. Allowed values are '{}'.".format( - value, attribute_name, self.name, allowed_values - ) + msg, ) if ( allowed_type is not None @@ -211,10 +227,9 @@ def set( if allowed_type == float and isinstance(value, int): value = float(value) else: + msg = f"Invalid value '{value}' for attribute '{attribute_name}' in variable '{self.name}'. Must be of type '{allowed_type}'." raise ValueError( - "Invalid value '{}' for attribute '{}' in variable '{}'. Must be of type '{}'.".format( - value, attribute_name, self.name, allowed_type - ) + msg, ) if setter is not None: value = setter(value) @@ -224,35 +239,38 @@ def set( def set_entity(self, entity): if not isinstance(entity, (Entity, GroupEntity)): - raise ValueError( + msg = ( f"Invalid value '{entity}' for attribute 'entity' in variable " f"'{self.name}'. Must be an instance of Entity or GroupEntity." ) + raise ValueError( + msg, + ) return entity def set_possible_values(self, possible_values): if not issubclass(possible_values, Enum): + msg = f"Invalid value '{possible_values}' for attribute 'possible_values' in variable '{self.name}'. Must be a subclass of {Enum}." raise ValueError( - "Invalid value '{}' for attribute 'possible_values' in variable '{}'. Must be a subclass of {}.".format( - possible_values, self.name, Enum - ) + msg, ) return possible_values def set_label(self, label): if label: return label + return None def set_end(self, end): if end: try: return datetime.datetime.strptime(end, "%Y-%m-%d").date() except ValueError: + msg = f"Incorrect 'end' attribute format in '{self.name}'. 'YYYY-MM-DD' expected where YYYY, MM and DD are year, month and day. Found: {end}" raise ValueError( - "Incorrect 'end' attribute format in '{}'. 'YYYY-MM-DD' expected where YYYY, MM and DD are year, month and day. Found: {}".format( - self.name, end - ) + msg, ) + return None def set_reference(self, reference): if reference: @@ -263,18 +281,16 @@ def set_reference(self, reference): elif isinstance(reference, tuple): reference = list(reference) else: + msg = f"The reference of the variable {self.name} is a {type(reference)} instead of a String or a List of Strings." raise TypeError( - "The reference of the variable {} is a {} instead of a String or a List of Strings.".format( - self.name, type(reference) - ) + msg, ) for element in reference: if not isinstance(element, str): + msg = f"The reference of the variable {self.name} is a {type(reference)} instead of a String or a List of Strings." raise TypeError( - "The reference of the variable {} is a {} instead of a String or a List of Strings.".format( - self.name, type(reference) - ) + msg, ) return reference @@ -282,6 +298,7 @@ def set_reference(self, reference): def set_documentation(self, documentation): if documentation: return textwrap.dedent(documentation) + return None def set_set_input(self, set_input): if not set_input and self.baseline_variable: @@ -299,10 +316,9 @@ def set_formulas(self, formulas_attr): starting_date = self.parse_formula_name(formula_name) if self.end is not None and starting_date > self.end: + msg = f'You declared that "{self.name}" ends on "{self.end}", but you wrote a formula to calculate it from "{starting_date}" ({formula_name}). The "end" attribute of a variable must be posterior to the start dates of all its formulas.' raise ValueError( - 'You declared that "{}" ends on "{}", but you wrote a formula to calculate it from "{}" ({}). The "end" attribute of a variable must be posterior to the start dates of all its formulas.'.format( - self.name, self.end, starting_date, formula_name - ) + msg, ) formulas[str(starting_date)] = formula @@ -316,14 +332,13 @@ def set_formulas(self, formulas_attr): for baseline_start_date, baseline_formula in self.baseline_variable.formulas.items() if first_reform_formula_date is None or baseline_start_date < first_reform_formula_date - } + }, ) return formulas def parse_formula_name(self, attribute_name): - """ - Returns the starting date of a formula based on its name. + """Returns the starting date of a formula based on its name. Valid dated name formats are : 'formula', 'formula_YYYY', 'formula_YYYY_MM' and 'formula_YYYY_MM_DD' where YYYY, MM and DD are a year, month and day. @@ -333,11 +348,10 @@ def parse_formula_name(self, attribute_name): - `formula_YYYY_MM` is `YYYY-MM-01` """ - def raise_error(): + def raise_error() -> NoReturn: + msg = f'Unrecognized formula name in variable "{self.name}". Expecting "formula_YYYY" or "formula_YYYY_MM" or "formula_YYYY_MM_DD where YYYY, MM and DD are year, month and day. Found: "{attribute_name}".' raise ValueError( - 'Unrecognized formula name in variable "{}". Expecting "formula_YYYY" or "formula_YYYY_MM" or "formula_YYYY_MM_DD where YYYY, MM and DD are year, month and day. Found: "{}".'.format( - self.name, attribute_name - ) + msg, ) if attribute_name == config.FORMULA_NAME_PREFIX: @@ -349,7 +363,7 @@ def raise_error(): if not match: raise_error() date_str = "-".join( - [match.group(1), match.group(2) or "01", match.group(3) or "01"] + [match.group(1), match.group(2) or "01", match.group(3) or "01"], ) try: @@ -360,9 +374,7 @@ def raise_error(): # ----- Methods ----- # def is_input_variable(self): - """ - Returns True if the variable is an input variable. - """ + """Returns True if the variable is an input variable.""" return len(self.formulas) == 0 @classmethod @@ -374,8 +386,8 @@ def get_introspection_data(cls): def get_formula( self, - period: Union[Instant, Period, str, int] = None, - ) -> Optional[Formula]: + period: Instant | Period | str | int = None, + ) -> Formula | None: """Returns the formula to compute the variable at the given period. If no period is given and the variable has several formulas, the method @@ -388,14 +400,15 @@ def get_formula( Formula used to compute the variable. """ - - instant: Optional[Instant] + instant: Instant | None if not self.formulas: return None if period is None: - return self.formulas.peekitem(index=0)[ + return self.formulas.peekitem( + index=0, + )[ 1 ] # peekitem gets the 1st key-value tuple (the oldest start_date and formula). Return the formula. @@ -422,8 +435,7 @@ def get_formula( return None def clone(self): - clone = self.__class__() - return clone + return self.__class__() def check_set_value(self, value): if self.value_type == Enum and isinstance(value, str): @@ -431,39 +443,33 @@ def check_set_value(self, value): value = self.possible_values[value].index except KeyError: possible_values = [item.name for item in self.possible_values] + msg = "'{}' is not a known value for '{}'. Possible values are ['{}'].".format( + value, + self.name, + "', '".join(possible_values), + ) raise ValueError( - "'{}' is not a known value for '{}'. Possible values are ['{}'].".format( - value, self.name, "', '".join(possible_values) - ) + msg, ) if self.value_type in (float, int) and isinstance(value, str): try: value = tools.eval_expression(value) except SyntaxError: + msg = f"I couldn't understand '{value}' as a value for '{self.name}'" raise ValueError( - "I couldn't understand '{}' as a value for '{}'".format( - value, self.name - ) + msg, ) try: value = numpy.array([value], dtype=self.dtype)[0] except (TypeError, ValueError): if self.value_type == datetime.date: - error_message = "Can't deal with date: '{}'.".format(value) + error_message = f"Can't deal with date: '{value}'." else: - error_message = ( - "Can't deal with value: expected type {}, received '{}'.".format( - self.json_type, value - ) - ) + error_message = f"Can't deal with value: expected type {self.json_type}, received '{value}'." raise ValueError(error_message) except OverflowError: - error_message = ( - "Can't deal with value: '{}', it's too large for type '{}'.".format( - value, self.json_type - ) - ) + error_message = f"Can't deal with value: '{value}', it's too large for type '{self.json_type}'." raise ValueError(error_message) return value diff --git a/openfisca_core/warnings/libyaml_warning.py b/openfisca_core/warnings/libyaml_warning.py index 7bbf1a5610..7ea797b667 100644 --- a/openfisca_core/warnings/libyaml_warning.py +++ b/openfisca_core/warnings/libyaml_warning.py @@ -1,6 +1,2 @@ class LibYAMLWarning(UserWarning): - """ - Custom warning for LibYAML not installed. - """ - - pass + """Custom warning for LibYAML not installed.""" diff --git a/openfisca_core/warnings/memory_warning.py b/openfisca_core/warnings/memory_warning.py index ef4bcf28af..23e82bf3e0 100644 --- a/openfisca_core/warnings/memory_warning.py +++ b/openfisca_core/warnings/memory_warning.py @@ -1,6 +1,2 @@ class MemoryConfigWarning(UserWarning): - """ - Custom warning for MemoryConfig. - """ - - pass + """Custom warning for MemoryConfig.""" diff --git a/openfisca_core/warnings/tempfile_warning.py b/openfisca_core/warnings/tempfile_warning.py index 433cf54772..9f4aad3820 100644 --- a/openfisca_core/warnings/tempfile_warning.py +++ b/openfisca_core/warnings/tempfile_warning.py @@ -1,6 +1,2 @@ class TempfileWarning(UserWarning): - """ - Custom warning when using a tempfile on disk. - """ - - pass + """Custom warning when using a tempfile on disk.""" diff --git a/openfisca_web_api/app.py b/openfisca_web_api/app.py index b4682b17e7..a76f255a0c 100644 --- a/openfisca_web_api/app.py +++ b/openfisca_web_api/app.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import logging import os import traceback @@ -31,7 +29,7 @@ def init_tracker(url, idsite, tracker_token): "You chose to activate the `tracker` module. ", "Tracking data will be sent to: " + url, "For more information, see .", - ] + ], ) log.info(info) return tracker @@ -42,9 +40,9 @@ def init_tracker(url, idsite, tracker_token): traceback.format_exc(), "You chose to activate the `tracker` module, but it is not installed.", "For more information, see .", - ] + ], ) - log.warn(message) + log.warning(message) def create_app( @@ -77,6 +75,7 @@ def create_app( def before_request(): if request.path != "/" and request.path.endswith("/"): return redirect(request.path[:-1]) + return None @app.route("/") def get_root(): @@ -84,8 +83,8 @@ def get_root(): jsonify( { "welcome": welcome_message - or DEFAULT_WELCOME_MESSAGE.format(request.host_url) - } + or DEFAULT_WELCOME_MESSAGE.format(request.host_url), + }, ), 300, ) @@ -95,7 +94,7 @@ def get_parameters(): parameters = { parameter["id"]: { "description": parameter["description"], - "href": "{}parameter/{}".format(request.host_url, name), + "href": f"{request.host_url}parameter/{name}", } for name, parameter in data["parameters"].items() if parameter.get("subparams") @@ -120,7 +119,7 @@ def get_variables(): variables = { name: { "description": variable["description"], - "href": "{}variable/{}".format(request.host_url, name), + "href": f"{request.host_url}variable/{name}", } for name, variable in data["variables"].items() } @@ -146,15 +145,15 @@ def get_spec(): return jsonify( { **data["openAPI_spec"], - **{"servers": [{"url": url}]}, - } + "servers": [{"url": url}], + }, ) - def handle_invalid_json(error): + def handle_invalid_json(error) -> None: json_response = jsonify( { - "error": "Invalid JSON: {}".format(error.args[0]), - } + "error": f"Invalid JSON: {error.args[0]}", + }, ) abort(make_response(json_response, 400)) @@ -173,7 +172,7 @@ def calculate(): make_response( jsonify({"error": "'" + e[1] + "' is not a valid ASCII value."}), 400, - ) + ), ) return jsonify(result) @@ -194,7 +193,7 @@ def apply_headers(response): { "Country-Package": data["country_package_metadata"]["name"], "Country-Package-Version": data["country_package_metadata"]["version"], - } + }, ) return response diff --git a/openfisca_web_api/errors.py b/openfisca_web_api/errors.py index ba804a7b08..ac93ebd833 100644 --- a/openfisca_web_api/errors.py +++ b/openfisca_web_api/errors.py @@ -1,13 +1,12 @@ -# -*- coding: utf-8 -*- +from typing import NoReturn import logging log = logging.getLogger("gunicorn.error") -def handle_import_error(error): +def handle_import_error(error) -> NoReturn: + msg = f"OpenFisca is missing some dependencies to run the Web API: '{error}'. To install them, run `pip install openfisca_core[web-api]`." raise ImportError( - "OpenFisca is missing some dependencies to run the Web API: '{}'. To install them, run `pip install openfisca_core[web-api]`.".format( - error - ) + msg, ) diff --git a/openfisca_web_api/handlers.py b/openfisca_web_api/handlers.py index a336a490b0..2f6fc4403a 100644 --- a/openfisca_web_api/handlers.py +++ b/openfisca_web_api/handlers.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import dpath.util from openfisca_core.indexed_enums import Enum @@ -7,12 +5,13 @@ def calculate(tax_benefit_system, input_data: dict) -> dict: - """ - Returns the input_data where the None values are replaced by the calculated values. - """ + """Returns the input_data where the None values are replaced by the calculated values.""" simulation = SimulationBuilder().build_from_entities(tax_benefit_system, input_data) requested_computations = dpath.util.search( - input_data, "*/*/*/*", afilter=lambda t: t is None, yielded=True + input_data, + "*/*/*/*", + afilter=lambda t: t is None, + yielded=True, ) computation_results: dict = {} for computation in requested_computations: @@ -29,7 +28,7 @@ def calculate(tax_benefit_system, input_data: dict) -> dict: entity_result = result.decode()[entity_index].name elif variable.value_type == float: entity_result = float( - str(result[entity_index]) + str(result[entity_index]), ) # To turn the float32 into a regular float without adding confusing extra decimals. There must be a better way. elif variable.value_type == str: entity_result = str(result[entity_index]) @@ -40,27 +39,26 @@ def calculate(tax_benefit_system, input_data: dict) -> dict: # See https://github.com/dpath-maintainers/dpath-python/issues/160 if computation_results == {}: computation_results = { - entity_plural: {entity_id: {variable_name: {period: entity_result}}} + entity_plural: {entity_id: {variable_name: {period: entity_result}}}, } - else: - if entity_plural in computation_results: - if entity_id in computation_results[entity_plural]: - if variable_name in computation_results[entity_plural][entity_id]: - computation_results[entity_plural][entity_id][variable_name][ - period - ] = entity_result - else: - computation_results[entity_plural][entity_id][variable_name] = { - period: entity_result - } + elif entity_plural in computation_results: + if entity_id in computation_results[entity_plural]: + if variable_name in computation_results[entity_plural][entity_id]: + computation_results[entity_plural][entity_id][variable_name][ + period + ] = entity_result else: - computation_results[entity_plural][entity_id] = { - variable_name: {period: entity_result} + computation_results[entity_plural][entity_id][variable_name] = { + period: entity_result, } else: - computation_results[entity_plural] = { - entity_id: {variable_name: {period: entity_result}} + computation_results[entity_plural][entity_id] = { + variable_name: {period: entity_result}, } + else: + computation_results[entity_plural] = { + entity_id: {variable_name: {period: entity_result}}, + } dpath.util.merge(input_data, computation_results) return input_data @@ -72,12 +70,15 @@ def trace(tax_benefit_system, input_data): requested_calculations = [] requested_computations = dpath.util.search( - input_data, "*/*/*/*", afilter=lambda t: t is None, yielded=True + input_data, + "*/*/*/*", + afilter=lambda t: t is None, + yielded=True, ) for computation in requested_computations: path = computation[0] entity_plural, entity_id, variable_name, period = path.split("/") - requested_calculations.append(f"{variable_name}<{str(period)}>") + requested_calculations.append(f"{variable_name}<{period!s}>") simulation.calculate(variable_name, period) trace = simulation.tracer.get_serialized_flat_trace() diff --git a/openfisca_web_api/loader/__init__.py b/openfisca_web_api/loader/__init__.py index fcea068e21..8d9318d9ae 100644 --- a/openfisca_web_api/loader/__init__.py +++ b/openfisca_web_api/loader/__init__.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- - - from openfisca_web_api.loader.entities import build_entities from openfisca_web_api.loader.parameters import build_parameters from openfisca_web_api.loader.spec import build_openAPI_specification diff --git a/openfisca_web_api/loader/entities.py b/openfisca_web_api/loader/entities.py index 683537aa0e..98ce4e6fb9 100644 --- a/openfisca_web_api/loader/entities.py +++ b/openfisca_web_api/loader/entities.py @@ -1,11 +1,5 @@ -# -*- coding: utf-8 -*- - - def build_entities(tax_benefit_system): - entities = { - entity.key: build_entity(entity) for entity in tax_benefit_system.entities - } - return entities + return {entity.key: build_entity(entity) for entity in tax_benefit_system.entities} def build_entity(entity): diff --git a/openfisca_web_api/loader/parameters.py b/openfisca_web_api/loader/parameters.py index 8841f7ebe8..193f12915f 100644 --- a/openfisca_web_api/loader/parameters.py +++ b/openfisca_web_api/loader/parameters.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +import functools +import operator from openfisca_core.parameters import Parameter, ParameterNode, Scale @@ -24,8 +25,7 @@ def get_value(date, values): if candidates: return candidates[0][1] - else: - return None + return None def build_api_scale(scale, value_key_name): @@ -39,13 +39,14 @@ def build_api_scale(scale, value_key_name): ] dates = set( - sum( + functools.reduce( + operator.iadd, [ list(bracket["thresholds"].keys()) + list(bracket["values"].keys()) for bracket in brackets ], [], - ) + ), ) # flatten the dates and remove duplicates # We iterate on all dates as we need to build the whole scale for each of them @@ -87,7 +88,8 @@ def build_api_parameter(parameter, country_package_metadata): } if parameter.file_path: api_parameter["source"] = build_source_url( - parameter.file_path, country_package_metadata + parameter.file_path, + country_package_metadata, ) if isinstance(parameter, Parameter): if parameter.documentation: @@ -113,7 +115,8 @@ def build_api_parameter(parameter, country_package_metadata): def build_parameters(tax_benefit_system, country_package_metadata): return { parameter.name.replace(".", "/"): build_api_parameter( - parameter, country_package_metadata + parameter, + country_package_metadata, ) for parameter in tax_benefit_system.parameters.get_descendants() } diff --git a/openfisca_web_api/loader/spec.py b/openfisca_web_api/loader/spec.py index 335317d2fe..4a163bd91f 100644 --- a/openfisca_web_api/loader/spec.py +++ b/openfisca_web_api/loader/spec.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os from copy import deepcopy @@ -10,13 +8,15 @@ from openfisca_web_api import handlers OPEN_API_CONFIG_FILE = os.path.join( - os.path.dirname(os.path.abspath(__file__)), os.path.pardir, "openAPI.yml" + os.path.dirname(os.path.abspath(__file__)), + os.path.pardir, + "openAPI.yml", ) def build_openAPI_specification(api_data): tax_benefit_system = api_data["tax_benefit_system"] - file = open(OPEN_API_CONFIG_FILE, "r") + file = open(OPEN_API_CONFIG_FILE) spec = yaml.safe_load(file) country_package_name = api_data["country_package_metadata"]["name"].title() country_package_version = api_data["country_package_metadata"]["version"] @@ -29,21 +29,24 @@ def build_openAPI_specification(api_data): spec, "info/description", spec["info"]["description"].replace( - "{COUNTRY_PACKAGE_NAME}", country_package_name + "{COUNTRY_PACKAGE_NAME}", + country_package_name, ), ) dpath.util.new( spec, "info/version", spec["info"]["version"].replace( - "{COUNTRY_PACKAGE_VERSION}", country_package_version + "{COUNTRY_PACKAGE_VERSION}", + country_package_version, ), ) for entity in tax_benefit_system.entities: name = entity.key.title() spec["components"]["schemas"][name] = get_entity_json_schema( - entity, tax_benefit_system + entity, + tax_benefit_system, ) situation_schema = get_situation_json_schema(tax_benefit_system) @@ -79,7 +82,9 @@ def build_openAPI_specification(api_data): if tax_benefit_system.open_api_config.get("simulation_example"): simulation_example = tax_benefit_system.open_api_config["simulation_example"] dpath.util.new( - spec, "components/schemas/SituationInput/example", simulation_example + spec, + "components/schemas/SituationInput/example", + simulation_example, ) dpath.util.new( spec, @@ -92,9 +97,7 @@ def build_openAPI_specification(api_data): handlers.trace(tax_benefit_system, simulation_example), ) else: - message = "No simulation example has been defined for this tax and benefit system. If you are the maintainer of {}, you can define an example by following this documentation: https://openfisca.org/doc/openfisca-web-api/config-openapi.html".format( - country_package_name - ) + message = f"No simulation example has been defined for this tax and benefit system. If you are the maintainer of {country_package_name}, you can define an example by following this documentation: https://openfisca.org/doc/openfisca-web-api/config-openapi.html" dpath.util.new(spec, "components/schemas/SituationInput/example", message) dpath.util.new(spec, "components/schemas/SituationOutput/example", message) dpath.util.new(spec, "components/schemas/Trace/example", message) @@ -122,32 +125,31 @@ def get_entity_json_schema(entity, tax_benefit_system): "properties": { variable_name: get_variable_json_schema(variable) for variable_name, variable in tax_benefit_system.get_variables( - entity + entity, ).items() }, "additionalProperties": False, } - else: - properties = {} - properties.update( - { - role.plural or role.key: {"type": "array", "items": {"type": "string"}} - for role in entity.roles - } - ) - properties.update( - { - variable_name: get_variable_json_schema(variable) - for variable_name, variable in tax_benefit_system.get_variables( - entity - ).items() - } - ) - return { - "type": "object", - "properties": properties, - "additionalProperties": False, - } + properties = {} + properties.update( + { + role.plural or role.key: {"type": "array", "items": {"type": "string"}} + for role in entity.roles + }, + ) + properties.update( + { + variable_name: get_variable_json_schema(variable) + for variable_name, variable in tax_benefit_system.get_variables( + entity, + ).items() + }, + ) + return { + "type": "object", + "properties": properties, + "additionalProperties": False, + } def get_situation_json_schema(tax_benefit_system): @@ -158,7 +160,7 @@ def get_situation_json_schema(tax_benefit_system): entity.plural: { "type": "object", "additionalProperties": { - "$ref": "#/components/schemas/{}".format(entity.key.title()) + "$ref": f"#/components/schemas/{entity.key.title()}", }, } for entity in tax_benefit_system.entities diff --git a/openfisca_web_api/loader/tax_benefit_system.py b/openfisca_web_api/loader/tax_benefit_system.py index 3cbd0edb81..358f960501 100644 --- a/openfisca_web_api/loader/tax_benefit_system.py +++ b/openfisca_web_api/loader/tax_benefit_system.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import importlib import logging import traceback @@ -15,15 +13,15 @@ def build_tax_benefit_system(country_package_name): message = linesep.join( [ traceback.format_exc(), - "Could not import module `{}`.".format(country_package_name), + f"Could not import module `{country_package_name}`.", "Are you sure it is installed in your environment? If so, look at the stack trace above to determine the origin of this error.", "See more at .", linesep, - ] + ], ) raise ValueError(message) try: return country_package.CountryTaxBenefitSystem() except NameError: # Gunicorn swallows NameErrors. Force printing the stack trace. - log.error(traceback.format_exc()) + log.exception(traceback.format_exc()) raise diff --git a/openfisca_web_api/loader/variables.py b/openfisca_web_api/loader/variables.py index f9b6e05887..6730dc0811 100644 --- a/openfisca_web_api/loader/variables.py +++ b/openfisca_web_api/loader/variables.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import datetime import inspect import textwrap @@ -26,7 +24,10 @@ def get_default_value(variable): def build_source_url( - country_package_metadata, source_file_path, start_line_number, source_code + country_package_metadata, + source_file_path, + start_line_number, + source_code, ): nb_lines = source_code.count("\n") return "{}/blob/{}{}#L{}-L{}".format( @@ -45,7 +46,10 @@ def build_formula(formula, country_package_metadata, source_file_path): api_formula = { "source": build_source_url( - country_package_metadata, source_file_path, start_line_number, source_code + country_package_metadata, + source_file_path, + start_line_number, + source_code, ), "content": source_code, } @@ -80,7 +84,10 @@ def build_variable(variable, country_package_metadata): if source_code: result["source"] = build_source_url( - country_package_metadata, source_file_path, start_line_number, source_code + country_package_metadata, + source_file_path, + start_line_number, + source_code, ) if variable.documentation: @@ -91,7 +98,9 @@ def build_variable(variable, country_package_metadata): if len(variable.formulas) > 0: result["formulas"] = build_formulas( - variable.formulas, country_package_metadata, source_file_path + variable.formulas, + country_package_metadata, + source_file_path, ) if variable.end: diff --git a/openfisca_web_api/scripts/serve.py b/openfisca_web_api/scripts/serve.py index ea594d9d6c..6ba89f440a 100644 --- a/openfisca_web_api/scripts/serve.py +++ b/openfisca_web_api/scripts/serve.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import logging import sys @@ -33,7 +31,7 @@ def read_user_configuration(default_configuration, command_line_parser): if args.configuration_file: file_configuration = {} - with open(args.configuration_file, "r") as file: + with open(args.configuration_file) as file: exec(file.read(), {}, file_configuration) # Configuration file overloads default configuration @@ -43,7 +41,8 @@ def read_user_configuration(default_configuration, command_line_parser): gunicorn_parser = config.Config().parser() configuration = update(configuration, vars(args)) configuration = update( - configuration, vars(gunicorn_parser.parse_args(unknown_args)) + configuration, + vars(gunicorn_parser.parse_args(unknown_args)), ) if configuration["args"]: command_line_parser.print_help() @@ -59,17 +58,17 @@ def update(configuration, new_options): configuration[key] = value if key == "port": configuration["bind"] = configuration["bind"][:-4] + str( - configuration["port"] + configuration["port"], ) return configuration class OpenFiscaWebAPIApplication(BaseApplication): - def __init__(self, options): + def __init__(self, options) -> None: self.options = options - super(OpenFiscaWebAPIApplication, self).__init__() + super().__init__() - def load_config(self): + def load_config(self) -> None: for key, value in self.options.items(): if key in self.cfg.settings: self.cfg.set(key.lower(), value) @@ -89,10 +88,10 @@ def load(self): ) -def main(parser): +def main(parser) -> None: configuration = { "port": DEFAULT_PORT, - "bind": "{}:{}".format(HOST, DEFAULT_PORT), + "bind": f"{HOST}:{DEFAULT_PORT}", "workers": DEFAULT_WORKERS_NUMBER, "timeout": DEFAULT_TIMEOUT, } diff --git a/pyproject.toml b/pyproject.toml index 1e2a43ee4e..d44a6e119a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,11 @@ [tool.black] target-version = ["py39", "py310", "py311"] + +[tool.ruff] +target-version = "py39" + +[tool.ruff.format] +docstring-code-format = true + +[tool.ruff.lint] +select = ["ALL"] diff --git a/setup.cfg b/setup.cfg index cc850c06a1..596ce99153 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,11 +11,15 @@ convention = google docstring_style = google extend-ignore = D -ignore = E203, E501, F405, RST301, W503 +ignore = B019, E203, E501, F405, E701, E704, RST212, RST301, W503 in-place = true include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors max-line-length = 88 -per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 +per-file-ignores = + */types.py:D101,D102,E704 + */test_*.py:D101,D102,D103 + */__init__.py:F401 + */__init__.pyi:E302,E704 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, mod, obj strictness = short @@ -33,6 +37,7 @@ score = no [isort] case_sensitive = true +combine_as_imports = true force_alphabetical_sort_within_sections = false group_by_package = true honor_noqa = true @@ -41,6 +46,7 @@ known_first_party = openfisca_core known_openfisca = openfisca_country_template, openfisca_extension_template known_typing = *collections.abc*, *typing*, *typing_extensions* known_types = *types* +multi_line_output = 3 profile = black py_version = 39 sections = FUTURE, TYPING, TYPES, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER @@ -74,6 +80,7 @@ follow_imports = skip ignore_missing_imports = true implicit_reexport = false install_types = true +mypy_path = stubs non_interactive = true plugins = numpy.typing.mypy_plugin pretty = true diff --git a/setup.py b/setup.py index cca107bee8..2ddfd2c468 100644 --- a/setup.py +++ b/setup.py @@ -47,23 +47,25 @@ ] dev_requirements = [ - "black >=23.1.0, <24.0", - "coverage >=6.5.0, <7.0", + "black >=24.8.0, <25.0", + "coverage >=7.6.1, <8.0", "darglint >=1.8.1, <2.0", - "flake8 >=6.0.0, <7.0.0", - "flake8-bugbear >=23.3.23, <24.0", + "flake8 >=7.1.1, <8.0.0", + "flake8-bugbear >=24.8.19, <25.0", "flake8-docstrings >=1.7.0, <2.0", "flake8-print >=5.0.0, <6.0", "flake8-rst-docstrings >=0.3.0, <0.4.0", - "idna >=3.4, <4.0", - "isort >=5.12.0, <6.0", - "mypy >=1.1.1, <2.0", + "idna >=3.10, <4.0", + "isort >=5.13.2, <6.0", + "mypy >=1.11.2, <2.0", "openapi-spec-validator >=0.7.1, <0.8.0", - "pycodestyle >=2.10.0, <3.0", - "pylint >=2.17.1, <3.0", + "pylint >=3.3.1, <4.0", "pylint-per-file-ignores >=1.3.2, <2.0", - "xdoctest >=1.1.1, <2.0", -] + api_requirements + "pyright >=1.1.381, <2.0", + "ruff >=0.6.7, <1.0", + "xdoctest >=1.2.0, <2.0", + *api_requirements, +] setup( name="OpenFisca-Core", diff --git a/tests/core/parameter_validation/test_parameter_clone.py b/tests/core/parameter_validation/test_parameter_clone.py index 1c74d861a3..6c77b4bb0b 100644 --- a/tests/core/parameter_validation/test_parameter_clone.py +++ b/tests/core/parameter_validation/test_parameter_clone.py @@ -6,7 +6,7 @@ year = 2016 -def test_clone(): +def test_clone() -> None: path = os.path.join(BASE_DIR, "filesystem_hierarchy") parameters = ParameterNode("", directory_path=path) parameters_at_instant = parameters("2016-01-01") @@ -19,7 +19,7 @@ def test_clone(): assert id(clone.node1.param) != id(parameters.node1.param) -def test_clone_parameter(tax_benefit_system): +def test_clone_parameter(tax_benefit_system) -> None: param = tax_benefit_system.parameters.taxes.income_tax_rate clone = param.clone() @@ -30,7 +30,7 @@ def test_clone_parameter(tax_benefit_system): assert clone.values_list == param.values_list -def test_clone_parameter_node(tax_benefit_system): +def test_clone_parameter_node(tax_benefit_system) -> None: node = tax_benefit_system.parameters.taxes clone = node.clone() @@ -39,7 +39,7 @@ def test_clone_parameter_node(tax_benefit_system): assert clone.children["income_tax_rate"] is not node.children["income_tax_rate"] -def test_clone_scale(tax_benefit_system): +def test_clone_scale(tax_benefit_system) -> None: scale = tax_benefit_system.parameters.taxes.social_security_contribution clone = scale.clone() @@ -47,7 +47,7 @@ def test_clone_scale(tax_benefit_system): assert clone.brackets[0].rate is not scale.brackets[0].rate -def test_deep_edit(tax_benefit_system): +def test_deep_edit(tax_benefit_system) -> None: parameters = tax_benefit_system.parameters clone = parameters.clone() diff --git a/tests/core/parameter_validation/test_parameter_validation.py b/tests/core/parameter_validation/test_parameter_validation.py index f4b8a82d50..d3419312d2 100644 --- a/tests/core/parameter_validation/test_parameter_validation.py +++ b/tests/core/parameter_validation/test_parameter_validation.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os import pytest @@ -14,7 +12,7 @@ year = 2016 -def check_fails_with_message(file_name, keywords): +def check_fails_with_message(file_name, keywords) -> None: path = os.path.join(BASE_DIR, file_name) + ".yaml" try: load_parameter_file(path, file_name) @@ -65,24 +63,24 @@ def check_fails_with_message(file_name, keywords): ("duplicate_key", {"duplicate"}), ], ) -def test_parsing_errors(test): +def test_parsing_errors(test) -> None: with pytest.raises(ParameterParsingError): check_fails_with_message(*test) -def test_array_type(): +def test_array_type() -> None: path = os.path.join(BASE_DIR, "array_type.yaml") load_parameter_file(path, "array_type") -def test_filesystem_hierarchy(): +def test_filesystem_hierarchy() -> None: path = os.path.join(BASE_DIR, "filesystem_hierarchy") parameters = ParameterNode("", directory_path=path) parameters_at_instant = parameters("2016-01-01") assert parameters_at_instant.node1.param == 1.0 -def test_yaml_hierarchy(): +def test_yaml_hierarchy() -> None: path = os.path.join(BASE_DIR, "yaml_hierarchy") parameters = ParameterNode("", directory_path=path) parameters_at_instant = parameters("2016-01-01") diff --git a/tests/core/parameters_date_indexing/test_date_indexing.py b/tests/core/parameters_date_indexing/test_date_indexing.py index 05bb770823..cefec26648 100644 --- a/tests/core/parameters_date_indexing/test_date_indexing.py +++ b/tests/core/parameters_date_indexing/test_date_indexing.py @@ -16,10 +16,11 @@ def get_message(error): return error.args[0] -def test_on_leaf(): +def test_on_leaf() -> None: parameter_at_instant = parameters.full_rate_required_duration("1995-01-01") birthdate = numpy.array( - ["1930-01-01", "1935-01-01", "1940-01-01", "1945-01-01"], dtype="datetime64[D]" + ["1930-01-01", "1935-01-01", "1940-01-01", "1945-01-01"], + dtype="datetime64[D]", ) assert_near( parameter_at_instant.contribution_quarters_required_by_birthdate[birthdate], @@ -27,9 +28,10 @@ def test_on_leaf(): ) -def test_on_node(): +def test_on_node() -> None: birthdate = numpy.array( - ["1950-01-01", "1953-01-01", "1956-01-01", "1959-01-01"], dtype="datetime64[D]" + ["1950-01-01", "1953-01-01", "1956-01-01", "1959-01-01"], + dtype="datetime64[D]", ) parameter_at_instant = parameters.full_rate_age("2012-03-01") node = parameter_at_instant.full_rate_age_by_birthdate[birthdate] diff --git a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py index 73a4ccb323..b7e7cf4e45 100644 --- a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py +++ b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- - import os import re -import numpy as np +import numpy import pytest from openfisca_core.indexed_enums import Enum @@ -21,27 +19,27 @@ def get_message(error): return error.args[0] -def test_on_leaf(): - zone = np.asarray(["z1", "z2", "z2", "z1"]) +def test_on_leaf() -> None: + zone = numpy.asarray(["z1", "z2", "z2", "z1"]) assert_near(P.single.owner[zone], [100, 200, 200, 100]) -def test_on_node(): - housing_occupancy_status = np.asarray(["owner", "owner", "tenant", "tenant"]) +def test_on_node() -> None: + housing_occupancy_status = numpy.asarray(["owner", "owner", "tenant", "tenant"]) node = P.single[housing_occupancy_status] assert_near(node.z1, [100, 100, 300, 300]) assert_near(node["z1"], [100, 100, 300, 300]) -def test_double_fancy_indexing(): - zone = np.asarray(["z1", "z2", "z2", "z1"]) - housing_occupancy_status = np.asarray(["owner", "owner", "tenant", "tenant"]) +def test_double_fancy_indexing() -> None: + zone = numpy.asarray(["z1", "z2", "z2", "z1"]) + housing_occupancy_status = numpy.asarray(["owner", "owner", "tenant", "tenant"]) assert_near(P.single[housing_occupancy_status][zone], [100, 200, 400, 300]) -def test_double_fancy_indexing_on_node(): - family_status = np.asarray(["single", "couple", "single", "couple"]) - housing_occupancy_status = np.asarray(["owner", "owner", "tenant", "tenant"]) +def test_double_fancy_indexing_on_node() -> None: + family_status = numpy.asarray(["single", "couple", "single", "couple"]) + housing_occupancy_status = numpy.asarray(["owner", "owner", "tenant", "tenant"]) node = P[family_status][housing_occupancy_status] assert_near(node.z1, [100, 500, 300, 700]) assert_near(node["z1"], [100, 500, 300, 700]) @@ -49,28 +47,37 @@ def test_double_fancy_indexing_on_node(): assert_near(node["z2"], [200, 600, 400, 800]) -def test_triple_fancy_indexing(): - family_status = np.asarray( - ["single", "single", "single", "single", "couple", "couple", "couple", "couple"] +def test_triple_fancy_indexing() -> None: + family_status = numpy.asarray( + [ + "single", + "single", + "single", + "single", + "couple", + "couple", + "couple", + "couple", + ], ) - housing_occupancy_status = np.asarray( - ["owner", "owner", "tenant", "tenant", "owner", "owner", "tenant", "tenant"] + housing_occupancy_status = numpy.asarray( + ["owner", "owner", "tenant", "tenant", "owner", "owner", "tenant", "tenant"], ) - zone = np.asarray(["z1", "z2", "z1", "z2", "z1", "z2", "z1", "z2"]) + zone = numpy.asarray(["z1", "z2", "z1", "z2", "z1", "z2", "z1", "z2"]) assert_near( P[family_status][housing_occupancy_status][zone], [100, 200, 300, 400, 500, 600, 700, 800], ) -def test_wrong_key(): - zone = np.asarray(["z1", "z2", "z2", "toto"]) +def test_wrong_key() -> None: + zone = numpy.asarray(["z1", "z2", "z2", "toto"]) with pytest.raises(ParameterNotFound) as e: P.single.owner[zone] assert "'rate.single.owner.toto' was not found" in get_message(e.value) -def test_inhomogenous(): +def test_inhomogenous() -> None: parameters = ParameterNode(directory_path=LOCAL_DIR) parameters.rate.couple.owner.add_child( "toto", @@ -79,20 +86,20 @@ def test_inhomogenous(): { "values": { "2015-01-01": {"value": 1000}, - } + }, }, ), ) P = parameters.rate("2015-01-01") - housing_occupancy_status = np.asarray(["owner", "owner", "tenant", "tenant"]) + housing_occupancy_status = numpy.asarray(["owner", "owner", "tenant", "tenant"]) with pytest.raises(ValueError) as error: P.couple[housing_occupancy_status] assert "'rate.couple.owner.toto' exists" in get_message(error.value) assert "'rate.couple.tenant.toto' doesn't" in get_message(error.value) -def test_inhomogenous_2(): +def test_inhomogenous_2() -> None: parameters = ParameterNode(directory_path=LOCAL_DIR) parameters.rate.couple.tenant.add_child( "toto", @@ -101,20 +108,20 @@ def test_inhomogenous_2(): { "values": { "2015-01-01": {"value": 1000}, - } + }, }, ), ) P = parameters.rate("2015-01-01") - housing_occupancy_status = np.asarray(["owner", "owner", "tenant", "tenant"]) + housing_occupancy_status = numpy.asarray(["owner", "owner", "tenant", "tenant"]) with pytest.raises(ValueError) as e: P.couple[housing_occupancy_status] assert "'rate.couple.tenant.toto' exists" in get_message(e.value) assert "'rate.couple.owner.toto' doesn't" in get_message(e.value) -def test_inhomogenous_3(): +def test_inhomogenous_3() -> None: parameters = ParameterNode(directory_path=LOCAL_DIR) parameters.rate.couple.tenant.add_child( "z4", @@ -125,14 +132,14 @@ def test_inhomogenous_3(): "values": { "2015-01-01": {"value": 550}, "2016-01-01": {"value": 600}, - } - } + }, + }, }, ), ) P = parameters.rate("2015-01-01") - zone = np.asarray(["z1", "z2", "z2", "z1"]) + zone = numpy.asarray(["z1", "z2", "z2", "z1"]) with pytest.raises(ValueError) as e: P.couple.tenant[zone] assert "'rate.couple.tenant.z4' is a node" in get_message(e.value) @@ -142,28 +149,29 @@ def test_inhomogenous_3(): P_2 = parameters.local_tax("2015-01-01") -def test_with_properties_starting_by_number(): - city_code = np.asarray(["75012", "75007", "75015"]) +def test_with_properties_starting_by_number() -> None: + city_code = numpy.asarray(["75012", "75007", "75015"]) assert_near(P_2[city_code], [100, 300, 200]) P_3 = parameters.bareme("2015-01-01") -def test_with_bareme(): - city_code = np.asarray(["75012", "75007", "75015"]) +def test_with_bareme() -> None: + city_code = numpy.asarray(["75012", "75007", "75015"]) with pytest.raises(NotImplementedError) as e: P_3[city_code] assert re.findall( - r"'bareme.7501\d' is a 'MarginalRateTaxScale'", get_message(e.value) + r"'bareme.7501\d' is a 'MarginalRateTaxScale'", + get_message(e.value), ) assert "has not been implemented" in get_message(e.value) -def test_with_enum(): +def test_with_enum() -> None: class TypesZone(Enum): z1 = "Zone 1" z2 = "Zone 2" - zone = np.asarray([TypesZone.z1, TypesZone.z2, TypesZone.z2, TypesZone.z1]) + zone = numpy.asarray([TypesZone.z1, TypesZone.z2, TypesZone.z2, TypesZone.z1]) assert_near(P.single.owner[zone], [100, 200, 200, 100]) diff --git a/tests/core/tax_scales/test_abstract_rate_tax_scale.py b/tests/core/tax_scales/test_abstract_rate_tax_scale.py index ad755075be..c966aa30f3 100644 --- a/tests/core/tax_scales/test_abstract_rate_tax_scale.py +++ b/tests/core/tax_scales/test_abstract_rate_tax_scale.py @@ -3,7 +3,7 @@ from openfisca_core import taxscales -def test_abstract_tax_scale(): +def test_abstract_tax_scale() -> None: with pytest.warns(DeprecationWarning): result = taxscales.AbstractRateTaxScale() assert isinstance(result, taxscales.AbstractRateTaxScale) diff --git a/tests/core/tax_scales/test_abstract_tax_scale.py b/tests/core/tax_scales/test_abstract_tax_scale.py index f1bfc4e4af..aad04d58ed 100644 --- a/tests/core/tax_scales/test_abstract_tax_scale.py +++ b/tests/core/tax_scales/test_abstract_tax_scale.py @@ -3,7 +3,7 @@ from openfisca_core import taxscales -def test_abstract_tax_scale(): +def test_abstract_tax_scale() -> None: with pytest.warns(DeprecationWarning): result = taxscales.AbstractTaxScale() assert isinstance(result, taxscales.AbstractTaxScale) diff --git a/tests/core/tax_scales/test_linear_average_rate_tax_scale.py b/tests/core/tax_scales/test_linear_average_rate_tax_scale.py index 18e6bd5a4a..6205d6de9b 100644 --- a/tests/core/tax_scales/test_linear_average_rate_tax_scale.py +++ b/tests/core/tax_scales/test_linear_average_rate_tax_scale.py @@ -4,7 +4,7 @@ from openfisca_core import taxscales, tools -def test_bracket_indices(): +def test_bracket_indices() -> None: tax_base = numpy.array([0, 1, 2, 3, 4, 5]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) @@ -16,7 +16,7 @@ def test_bracket_indices(): tools.assert_near(result, [0, 0, 0, 1, 1, 2]) -def test_bracket_indices_with_factor(): +def test_bracket_indices_with_factor() -> None: tax_base = numpy.array([0, 1, 2, 3, 4, 5]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) @@ -28,7 +28,7 @@ def test_bracket_indices_with_factor(): tools.assert_near(result, [0, 0, 0, 0, 1, 1]) -def test_bracket_indices_with_round_decimals(): +def test_bracket_indices_with_round_decimals() -> None: tax_base = numpy.array([0, 1, 2, 3, 4, 5]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) @@ -40,7 +40,7 @@ def test_bracket_indices_with_round_decimals(): tools.assert_near(result, [0, 0, 1, 1, 2, 2]) -def test_bracket_indices_without_tax_base(): +def test_bracket_indices_without_tax_base() -> None: tax_base = numpy.array([]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) @@ -51,7 +51,7 @@ def test_bracket_indices_without_tax_base(): tax_scale.bracket_indices(tax_base) -def test_bracket_indices_without_brackets(): +def test_bracket_indices_without_brackets() -> None: tax_base = numpy.array([0, 1, 2, 3, 4, 5]) tax_scale = taxscales.LinearAverageRateTaxScale() @@ -59,7 +59,7 @@ def test_bracket_indices_without_brackets(): tax_scale.bracket_indices(tax_base) -def test_to_dict(): +def test_to_dict() -> None: tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) tax_scale.add_bracket(100, 0.1) @@ -69,7 +69,7 @@ def test_to_dict(): assert result == {"0": 0.0, "100": 0.1} -def test_to_marginal(): +def test_to_marginal() -> None: tax_base = numpy.array([1, 1.5, 2, 2.5]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) diff --git a/tests/core/tax_scales/test_marginal_amount_tax_scale.py b/tests/core/tax_scales/test_marginal_amount_tax_scale.py index e00a8371c4..0a3275c901 100644 --- a/tests/core/tax_scales/test_marginal_amount_tax_scale.py +++ b/tests/core/tax_scales/test_marginal_amount_tax_scale.py @@ -15,12 +15,12 @@ def data(): "amount": { "2017-10-01": {"value": 6}, }, - } + }, ], } -def test_calc(): +def test_calc() -> None: tax_base = array([1, 8, 10]) tax_scale = taxscales.MarginalAmountTaxScale() tax_scale.add_bracket(6, 0.23) @@ -32,7 +32,7 @@ def test_calc(): # TODO: move, as we're testing Scale, not MarginalAmountTaxScale -def test_dispatch_scale_type_on_creation(data): +def test_dispatch_scale_type_on_creation(data) -> None: scale = parameters.Scale("amount_scale", data, "") first_jan = periods.Instant((2017, 11, 1)) diff --git a/tests/core/tax_scales/test_marginal_rate_tax_scale.py b/tests/core/tax_scales/test_marginal_rate_tax_scale.py index 3ed4a3f12f..7696e95fc4 100644 --- a/tests/core/tax_scales/test_marginal_rate_tax_scale.py +++ b/tests/core/tax_scales/test_marginal_rate_tax_scale.py @@ -4,7 +4,7 @@ from openfisca_core import taxscales, tools -def test_bracket_indices(): +def test_bracket_indices() -> None: tax_base = numpy.array([0, 1, 2, 3, 4, 5]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) @@ -16,7 +16,7 @@ def test_bracket_indices(): tools.assert_near(result, [0, 0, 0, 1, 1, 2]) -def test_bracket_indices_with_factor(): +def test_bracket_indices_with_factor() -> None: tax_base = numpy.array([0, 1, 2, 3, 4, 5]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) @@ -28,7 +28,7 @@ def test_bracket_indices_with_factor(): tools.assert_near(result, [0, 0, 0, 0, 1, 1]) -def test_bracket_indices_with_round_decimals(): +def test_bracket_indices_with_round_decimals() -> None: tax_base = numpy.array([0, 1, 2, 3, 4, 5]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) @@ -40,7 +40,7 @@ def test_bracket_indices_with_round_decimals(): tools.assert_near(result, [0, 0, 1, 1, 2, 2]) -def test_bracket_indices_without_tax_base(): +def test_bracket_indices_without_tax_base() -> None: tax_base = numpy.array([]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) @@ -51,7 +51,7 @@ def test_bracket_indices_without_tax_base(): tax_scale.bracket_indices(tax_base) -def test_bracket_indices_without_brackets(): +def test_bracket_indices_without_brackets() -> None: tax_base = numpy.array([0, 1, 2, 3, 4, 5]) tax_scale = taxscales.LinearAverageRateTaxScale() @@ -59,7 +59,7 @@ def test_bracket_indices_without_brackets(): tax_scale.bracket_indices(tax_base) -def test_to_dict(): +def test_to_dict() -> None: tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) tax_scale.add_bracket(100, 0.1) @@ -69,7 +69,7 @@ def test_to_dict(): assert result == {"0": 0.0, "100": 0.1} -def test_calc(): +def test_calc() -> None: tax_base = numpy.array([1, 1.5, 2, 2.5, 3.0, 4.0]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) @@ -86,7 +86,7 @@ def test_calc(): ) -def test_calc_without_round(): +def test_calc_without_round() -> None: tax_base = numpy.array([200, 200.2, 200.002, 200.6, 200.006, 200.5, 200.005]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) @@ -101,7 +101,7 @@ def test_calc_without_round(): ) -def test_calc_when_round_is_1(): +def test_calc_when_round_is_1() -> None: tax_base = numpy.array([200, 200.2, 200.002, 200.6, 200.006, 200.5, 200.005]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) @@ -116,7 +116,7 @@ def test_calc_when_round_is_1(): ) -def test_calc_when_round_is_2(): +def test_calc_when_round_is_2() -> None: tax_base = numpy.array([200, 200.2, 200.002, 200.6, 200.006, 200.5, 200.005]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) @@ -131,7 +131,7 @@ def test_calc_when_round_is_2(): ) -def test_calc_when_round_is_3(): +def test_calc_when_round_is_3() -> None: tax_base = numpy.array([200, 200.2, 200.002, 200.6, 200.006, 200.5, 200.005]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) @@ -146,7 +146,7 @@ def test_calc_when_round_is_3(): ) -def test_marginal_rates(): +def test_marginal_rates() -> None: tax_base = numpy.array([0, 10, 50, 125, 250]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) @@ -158,7 +158,7 @@ def test_marginal_rates(): tools.assert_near(result, [0, 0, 0, 0.1, 0.2]) -def test_inverse(): +def test_inverse() -> None: gross_tax_base = numpy.array([1, 2, 3, 4, 5, 6]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) @@ -171,7 +171,7 @@ def test_inverse(): tools.assert_near(result.calc(net_tax_base), gross_tax_base, 1e-15) -def test_scale_tax_scales(): +def test_scale_tax_scales() -> None: tax_base = numpy.array([1, 2, 3]) tax_base_scale = 12.345 scaled_tax_base = tax_base * tax_base_scale @@ -185,7 +185,7 @@ def test_scale_tax_scales(): tools.assert_near(result.thresholds, scaled_tax_base) -def test_inverse_scaled_marginal_tax_scales(): +def test_inverse_scaled_marginal_tax_scales() -> None: gross_tax_base = numpy.array([1, 2, 3, 4, 5, 6]) gross_tax_base_scale = 12.345 scaled_gross_tax_base = gross_tax_base * gross_tax_base_scale @@ -195,7 +195,7 @@ def test_inverse_scaled_marginal_tax_scales(): tax_scale.add_bracket(3, 0.05) scaled_tax_scale = tax_scale.scale_tax_scales(gross_tax_base_scale) scaled_net_tax_base = +scaled_gross_tax_base - scaled_tax_scale.calc( - scaled_gross_tax_base + scaled_gross_tax_base, ) result = scaled_tax_scale.inverse() @@ -203,7 +203,7 @@ def test_inverse_scaled_marginal_tax_scales(): tools.assert_near(result.calc(scaled_net_tax_base), scaled_gross_tax_base, 1e-13) -def test_to_average(): +def test_to_average() -> None: tax_base = numpy.array([1, 1.5, 2, 2.5]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) @@ -222,7 +222,7 @@ def test_to_average(): ) -def test_rate_from_bracket_indice(): +def test_rate_from_bracket_indice() -> None: tax_base = numpy.array([0, 1_000, 1_500, 50_000]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) @@ -236,7 +236,7 @@ def test_rate_from_bracket_indice(): assert (result == numpy.array([0.0, 0.1, 0.1, 0.4])).all() -def test_rate_from_tax_base(): +def test_rate_from_tax_base() -> None: tax_base = numpy.array([0, 3_000, 15_500, 500_000]) tax_scale = taxscales.MarginalRateTaxScale() tax_scale.add_bracket(0, 0) diff --git a/tests/core/tax_scales/test_rate_tax_scale_like.py b/tests/core/tax_scales/test_rate_tax_scale_like.py index 075fc802d2..9f5bc61286 100644 --- a/tests/core/tax_scales/test_rate_tax_scale_like.py +++ b/tests/core/tax_scales/test_rate_tax_scale_like.py @@ -3,7 +3,7 @@ from openfisca_core import taxscales -def test_threshold_from_tax_base(): +def test_threshold_from_tax_base() -> None: tax_base = numpy.array([0, 33_000, 500, 400_000]) tax_scale = taxscales.LinearAverageRateTaxScale() tax_scale.add_bracket(0, 0) diff --git a/tests/core/tax_scales/test_single_amount_tax_scale.py b/tests/core/tax_scales/test_single_amount_tax_scale.py index ffcd32e092..2b384f6374 100644 --- a/tests/core/tax_scales/test_single_amount_tax_scale.py +++ b/tests/core/tax_scales/test_single_amount_tax_scale.py @@ -19,12 +19,12 @@ def data(): "amount": { "2017-10-01": {"value": 6}, }, - } + }, ], } -def test_calc(): +def test_calc() -> None: tax_base = numpy.array([1, 8, 10]) tax_scale = taxscales.SingleAmountTaxScale() tax_scale.add_bracket(6, 0.23) @@ -35,7 +35,7 @@ def test_calc(): tools.assert_near(result, [0, 0.23, 0.29]) -def test_to_dict(): +def test_to_dict() -> None: tax_scale = taxscales.SingleAmountTaxScale() tax_scale.add_bracket(6, 0.23) tax_scale.add_bracket(9, 0.29) @@ -46,7 +46,7 @@ def test_to_dict(): # TODO: move, as we're testing Scale, not SingleAmountTaxScale -def test_assign_thresholds_on_creation(data): +def test_assign_thresholds_on_creation(data) -> None: scale = parameters.Scale("amount_scale", data, "") first_jan = periods.Instant((2017, 11, 1)) scale_at_instant = scale.get_at_instant(first_jan) @@ -57,7 +57,7 @@ def test_assign_thresholds_on_creation(data): # TODO: move, as we're testing Scale, not SingleAmountTaxScale -def test_assign_amounts_on_creation(data): +def test_assign_amounts_on_creation(data) -> None: scale = parameters.Scale("amount_scale", data, "") first_jan = periods.Instant((2017, 11, 1)) scale_at_instant = scale.get_at_instant(first_jan) @@ -68,7 +68,7 @@ def test_assign_amounts_on_creation(data): # TODO: move, as we're testing Scale, not SingleAmountTaxScale -def test_dispatch_scale_type_on_creation(data): +def test_dispatch_scale_type_on_creation(data) -> None: scale = parameters.Scale("amount_scale", data, "") first_jan = periods.Instant((2017, 11, 1)) diff --git a/tests/core/tax_scales/test_tax_scales_commons.py b/tests/core/tax_scales/test_tax_scales_commons.py index e4426cd49d..544e5a07fe 100644 --- a/tests/core/tax_scales/test_tax_scales_commons.py +++ b/tests/core/tax_scales/test_tax_scales_commons.py @@ -12,19 +12,19 @@ def node(): "brackets": [ {"rate": {"2015-01-01": 0.05}, "threshold": {"2015-01-01": 0}}, {"rate": {"2015-01-01": 0.10}, "threshold": {"2015-01-01": 2000}}, - ] + ], }, "retirement": { "brackets": [ {"rate": {"2015-01-01": 0.02}, "threshold": {"2015-01-01": 0}}, {"rate": {"2015-01-01": 0.04}, "threshold": {"2015-01-01": 3000}}, - ] + ], }, }, )(2015) -def test_combine_tax_scales(node): +def test_combine_tax_scales(node) -> None: result = taxscales.combine_tax_scales(node) tools.assert_near(result.thresholds, [0, 2000, 3000]) diff --git a/tests/core/test_axes.py b/tests/core/test_axes.py index 799439e9c4..11590daf51 100644 --- a/tests/core/test_axes.py +++ b/tests/core/test_axes.py @@ -7,46 +7,47 @@ # With periods -def test_add_axis_without_period(persons): +def test_add_axis_without_period(persons) -> None: simulation_builder = SimulationBuilder() simulation_builder.set_default_period("2018-11") simulation_builder.add_person_entity(persons, {"Alicia": {}}) simulation_builder.register_variable("salary", persons) simulation_builder.add_parallel_axis( - {"count": 3, "name": "salary", "min": 0, "max": 3000} + {"count": 3, "name": "salary", "min": 0, "max": 3000}, ) simulation_builder.expand_axes() assert simulation_builder.get_input("salary", "2018-11") == pytest.approx( - [0, 1500, 3000] + [0, 1500, 3000], ) # With variables -def test_add_axis_on_a_non_existing_variable(persons): +def test_add_axis_on_a_non_existing_variable(persons) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, {"Alicia": {}}) simulation_builder.add_parallel_axis( - {"count": 3, "name": "ubi", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 3, "name": "ubi", "min": 0, "max": 3000, "period": "2018-11"}, ) with pytest.raises(KeyError): simulation_builder.expand_axes() -def test_add_axis_on_an_existing_variable_with_input(persons): +def test_add_axis_on_an_existing_variable_with_input(persons) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity( - persons, {"Alicia": {"salary": {"2018-11": 1000}}} + persons, + {"Alicia": {"salary": {"2018-11": 1000}}}, ) simulation_builder.register_variable("salary", persons) simulation_builder.add_parallel_axis( - {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert simulation_builder.get_input("salary", "2018-11") == pytest.approx( - [0, 1500, 3000] + [0, 1500, 3000], ) assert simulation_builder.get_count("persons") == 3 assert simulation_builder.get_ids("persons") == ["Alicia0", "Alicia1", "Alicia2"] @@ -55,46 +56,46 @@ def test_add_axis_on_an_existing_variable_with_input(persons): # With entities -def test_add_axis_on_persons(persons): +def test_add_axis_on_persons(persons) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, {"Alicia": {}}) simulation_builder.register_variable("salary", persons) simulation_builder.add_parallel_axis( - {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert simulation_builder.get_input("salary", "2018-11") == pytest.approx( - [0, 1500, 3000] + [0, 1500, 3000], ) assert simulation_builder.get_count("persons") == 3 assert simulation_builder.get_ids("persons") == ["Alicia0", "Alicia1", "Alicia2"] -def test_add_two_axes(persons): +def test_add_two_axes(persons) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, {"Alicia": {}}) simulation_builder.register_variable("salary", persons) simulation_builder.add_parallel_axis( - {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.add_parallel_axis( - {"count": 3, "name": "pension", "min": 0, "max": 2000, "period": "2018-11"} + {"count": 3, "name": "pension", "min": 0, "max": 2000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert simulation_builder.get_input("salary", "2018-11") == pytest.approx( - [0, 1500, 3000] + [0, 1500, 3000], ) assert simulation_builder.get_input("pension", "2018-11") == pytest.approx( - [0, 1000, 2000] + [0, 1000, 2000], ) -def test_add_axis_with_group(persons): +def test_add_axis_with_group(persons) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, {"Alicia": {}, "Javier": {}}) simulation_builder.register_variable("salary", persons) simulation_builder.add_parallel_axis( - {"count": 2, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 2, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.add_parallel_axis( { @@ -104,7 +105,7 @@ def test_add_axis_with_group(persons): "max": 3000, "period": "2018-11", "index": 1, - } + }, ) simulation_builder.expand_axes() assert simulation_builder.get_count("persons") == 4 @@ -115,16 +116,16 @@ def test_add_axis_with_group(persons): "Javier3", ] assert simulation_builder.get_input("salary", "2018-11") == pytest.approx( - [0, 0, 3000, 3000] + [0, 0, 3000, 3000], ) -def test_add_axis_with_group_int_period(persons): +def test_add_axis_with_group_int_period(persons) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, {"Alicia": {}, "Javier": {}}) simulation_builder.register_variable("salary", persons) simulation_builder.add_parallel_axis( - {"count": 2, "name": "salary", "min": 0, "max": 3000, "period": 2018} + {"count": 2, "name": "salary", "min": 0, "max": 3000, "period": 2018}, ) simulation_builder.add_parallel_axis( { @@ -134,18 +135,19 @@ def test_add_axis_with_group_int_period(persons): "max": 3000, "period": 2018, "index": 1, - } + }, ) simulation_builder.expand_axes() assert simulation_builder.get_input("salary", "2018") == pytest.approx( - [0, 0, 3000, 3000] + [0, 0, 3000, 3000], ) -def test_add_axis_on_households(persons, households): +def test_add_axis_on_households(persons, households) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity( - persons, {"Alicia": {}, "Javier": {}, "Tom": {}} + persons, + {"Alicia": {}, "Javier": {}, "Tom": {}}, ) simulation_builder.add_group_entity( "persons", @@ -158,7 +160,7 @@ def test_add_axis_on_households(persons, households): ) simulation_builder.register_variable("rent", households) simulation_builder.add_parallel_axis( - {"count": 2, "name": "rent", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 2, "name": "rent", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert simulation_builder.get_count("households") == 4 @@ -169,14 +171,15 @@ def test_add_axis_on_households(persons, households): "houseb3", ] assert simulation_builder.get_input("rent", "2018-11") == pytest.approx( - [0, 0, 3000, 0] + [0, 0, 3000, 0], ) -def test_axis_on_group_expands_persons(persons, households): +def test_axis_on_group_expands_persons(persons, households) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity( - persons, {"Alicia": {}, "Javier": {}, "Tom": {}} + persons, + {"Alicia": {}, "Javier": {}, "Tom": {}}, ) simulation_builder.add_group_entity( "persons", @@ -189,16 +192,17 @@ def test_axis_on_group_expands_persons(persons, households): ) simulation_builder.register_variable("rent", households) simulation_builder.add_parallel_axis( - {"count": 2, "name": "rent", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 2, "name": "rent", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert simulation_builder.get_count("persons") == 6 -def test_add_axis_distributes_roles(persons, households): +def test_add_axis_distributes_roles(persons, households) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity( - persons, {"Alicia": {}, "Javier": {}, "Tom": {}} + persons, + {"Alicia": {}, "Javier": {}, "Tom": {}}, ) simulation_builder.add_group_entity( "persons", @@ -211,7 +215,7 @@ def test_add_axis_distributes_roles(persons, households): ) simulation_builder.register_variable("rent", households) simulation_builder.add_parallel_axis( - {"count": 2, "name": "rent", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 2, "name": "rent", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert [role.key for role in simulation_builder.get_roles("households")] == [ @@ -224,10 +228,11 @@ def test_add_axis_distributes_roles(persons, households): ] -def test_add_axis_on_persons_distributes_roles(persons, households): +def test_add_axis_on_persons_distributes_roles(persons, households) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity( - persons, {"Alicia": {}, "Javier": {}, "Tom": {}} + persons, + {"Alicia": {}, "Javier": {}, "Tom": {}}, ) simulation_builder.add_group_entity( "persons", @@ -240,7 +245,7 @@ def test_add_axis_on_persons_distributes_roles(persons, households): ) simulation_builder.register_variable("salary", persons) simulation_builder.add_parallel_axis( - {"count": 2, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 2, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert [role.key for role in simulation_builder.get_roles("households")] == [ @@ -253,10 +258,11 @@ def test_add_axis_on_persons_distributes_roles(persons, households): ] -def test_add_axis_distributes_memberships(persons, households): +def test_add_axis_distributes_memberships(persons, households) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity( - persons, {"Alicia": {}, "Javier": {}, "Tom": {}} + persons, + {"Alicia": {}, "Javier": {}, "Tom": {}}, ) simulation_builder.add_group_entity( "persons", @@ -269,33 +275,33 @@ def test_add_axis_distributes_memberships(persons, households): ) simulation_builder.register_variable("rent", households) simulation_builder.add_parallel_axis( - {"count": 2, "name": "rent", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 2, "name": "rent", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert simulation_builder.get_memberships("households") == [0, 1, 1, 2, 3, 3] -def test_add_perpendicular_axes(persons): +def test_add_perpendicular_axes(persons) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, {"Alicia": {}}) simulation_builder.register_variable("salary", persons) simulation_builder.register_variable("pension", persons) simulation_builder.add_parallel_axis( - {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.add_perpendicular_axis( - {"count": 2, "name": "pension", "min": 0, "max": 2000, "period": "2018-11"} + {"count": 2, "name": "pension", "min": 0, "max": 2000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert simulation_builder.get_input("salary", "2018-11") == pytest.approx( - [0, 1500, 3000, 0, 1500, 3000] + [0, 1500, 3000, 0, 1500, 3000], ) assert simulation_builder.get_input("pension", "2018-11") == pytest.approx( - [0, 0, 0, 2000, 2000, 2000] + [0, 0, 0, 2000, 2000, 2000], ) -def test_add_perpendicular_axis_on_an_existing_variable_with_input(persons): +def test_add_perpendicular_axis_on_an_existing_variable_with_input(persons) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_person_entity( persons, @@ -309,24 +315,24 @@ def test_add_perpendicular_axis_on_an_existing_variable_with_input(persons): simulation_builder.register_variable("salary", persons) simulation_builder.register_variable("pension", persons) simulation_builder.add_parallel_axis( - {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"} + {"count": 3, "name": "salary", "min": 0, "max": 3000, "period": "2018-11"}, ) simulation_builder.add_perpendicular_axis( - {"count": 2, "name": "pension", "min": 0, "max": 2000, "period": "2018-11"} + {"count": 2, "name": "pension", "min": 0, "max": 2000, "period": "2018-11"}, ) simulation_builder.expand_axes() assert simulation_builder.get_input("salary", "2018-11") == pytest.approx( - [0, 1500, 3000, 0, 1500, 3000] + [0, 1500, 3000, 0, 1500, 3000], ) assert simulation_builder.get_input("pension", "2018-11") == pytest.approx( - [0, 0, 0, 2000, 2000, 2000] + [0, 0, 0, 2000, 2000, 2000], ) # Integration tests -def test_simulation_with_axes(tax_benefit_system): +def test_simulation_with_axes(tax_benefit_system) -> None: input_yaml = """ persons: Alicia: {salary: {2018-11: 0}} @@ -348,7 +354,7 @@ def test_simulation_with_axes(tax_benefit_system): data = test_runner.yaml.safe_load(input_yaml) simulation = SimulationBuilder().build_from_dict(tax_benefit_system, data) assert simulation.get_array("salary", "2018-11") == pytest.approx( - [0, 0, 0, 0, 0, 0] + [0, 0, 0, 0, 0, 0], ) assert simulation.get_array("rent", "2018-11") == pytest.approx([0, 0, 3000, 0]) @@ -356,7 +362,7 @@ def test_simulation_with_axes(tax_benefit_system): # Test for missing group entities with build_from_entities() -def test_simulation_with_axes_missing_entities(tax_benefit_system): +def test_simulation_with_axes_missing_entities(tax_benefit_system) -> None: input_yaml = """ persons: Alicia: {salary: {2018-11: 0}} diff --git a/tests/core/test_calculate_output.py b/tests/core/test_calculate_output.py index ecf59b5f7d..54d868ba92 100644 --- a/tests/core/test_calculate_output.py +++ b/tests/core/test_calculate_output.py @@ -29,7 +29,7 @@ class variable_with_calculate_output_divide(Variable): @pytest.fixture(scope="module", autouse=True) -def add_variables_to_tax_benefit_system(tax_benefit_system): +def add_variables_to_tax_benefit_system(tax_benefit_system) -> None: tax_benefit_system.add_variables( simple_variable, variable_with_calculate_output_add, @@ -40,25 +40,27 @@ def add_variables_to_tax_benefit_system(tax_benefit_system): @pytest.fixture def simulation(tax_benefit_system): return SimulationBuilder().build_from_entities( - tax_benefit_system, situation_examples.single + tax_benefit_system, + situation_examples.single, ) -def test_calculate_output_default(simulation): +def test_calculate_output_default(simulation) -> None: with pytest.raises(ValueError): simulation.calculate_output("simple_variable", 2017) -def test_calculate_output_add(simulation): +def test_calculate_output_add(simulation) -> None: simulation.set_input("variable_with_calculate_output_add", "2017-01", [10]) simulation.set_input("variable_with_calculate_output_add", "2017-05", [20]) simulation.set_input("variable_with_calculate_output_add", "2017-12", [70]) tools.assert_near( - simulation.calculate_output("variable_with_calculate_output_add", 2017), 100 + simulation.calculate_output("variable_with_calculate_output_add", 2017), + 100, ) -def test_calculate_output_divide(simulation): +def test_calculate_output_divide(simulation) -> None: simulation.set_input("variable_with_calculate_output_divide", 2017, [12000]) tools.assert_near( simulation.calculate_output("variable_with_calculate_output_divide", "2017-06"), diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index 8263ac3c44..d206a8cb35 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -10,19 +10,19 @@ @pytest.mark.parametrize("simulation", [({"salary": 2000}, PERIOD)], indirect=True) -def test_input_variable(simulation): +def test_input_variable(simulation) -> None: result = simulation.calculate("salary", PERIOD) tools.assert_near(result, [2000], absolute_error_margin=0.01) @pytest.mark.parametrize("simulation", [({"salary": 2000}, PERIOD)], indirect=True) -def test_basic_calculation(simulation): +def test_basic_calculation(simulation) -> None: result = simulation.calculate("income_tax", PERIOD) tools.assert_near(result, [300], absolute_error_margin=0.01) @pytest.mark.parametrize("simulation", [({"salary": 24000}, PERIOD)], indirect=True) -def test_calculate_add(simulation): +def test_calculate_add(simulation) -> None: result = simulation.calculate_add("income_tax", PERIOD) tools.assert_near(result, [3600], absolute_error_margin=0.01) @@ -32,26 +32,26 @@ def test_calculate_add(simulation): [({"accommodation_size": 100, "housing_occupancy_status": "tenant"}, PERIOD)], indirect=True, ) -def test_calculate_divide(simulation): +def test_calculate_divide(simulation) -> None: result = simulation.calculate_divide("housing_tax", PERIOD) tools.assert_near(result, [1000 / 12.0], absolute_error_margin=0.01) @pytest.mark.parametrize("simulation", [({"salary": 20000}, PERIOD)], indirect=True) -def test_bareme(simulation): +def test_bareme(simulation) -> None: result = simulation.calculate("social_security_contribution", PERIOD) expected = [0.02 * 6000 + 0.06 * 6400 + 0.12 * 7600] tools.assert_near(result, expected, absolute_error_margin=0.01) @pytest.mark.parametrize("simulation", [({}, PERIOD)], indirect=True) -def test_non_existing_variable(simulation): +def test_non_existing_variable(simulation) -> None: with pytest.raises(VariableNotFoundError): simulation.calculate("non_existent_variable", PERIOD) @pytest.mark.parametrize("simulation", [({}, PERIOD)], indirect=True) -def test_calculate_variable_with_wrong_definition_period(simulation): +def test_calculate_variable_with_wrong_definition_period(simulation) -> None: year = str(PERIOD.this_year) with pytest.raises(ValueError) as error: @@ -67,7 +67,7 @@ def test_calculate_variable_with_wrong_definition_period(simulation): @pytest.mark.parametrize("simulation", [({}, PERIOD)], indirect=True) -def test_divide_option_with_complex_period(simulation): +def test_divide_option_with_complex_period(simulation) -> None: quarter = PERIOD.last_3_months with pytest.raises(ValueError) as error: @@ -82,7 +82,7 @@ def test_divide_option_with_complex_period(simulation): ), f"Expected '{word}' in error message '{error_message}'" -def test_input_with_wrong_period(tax_benefit_system): +def test_input_with_wrong_period(tax_benefit_system) -> None: year = str(PERIOD.this_year) variables = {"basic_income": {year: 12000}} simulation_builder = SimulationBuilder() @@ -92,7 +92,7 @@ def test_input_with_wrong_period(tax_benefit_system): simulation_builder.build_from_variables(tax_benefit_system, variables) -def test_variable_with_reference(make_simulation, isolated_tax_benefit_system): +def test_variable_with_reference(make_simulation, isolated_tax_benefit_system) -> None: variables = {"salary": 4000} simulation = make_simulation(isolated_tax_benefit_system, variables, PERIOD) @@ -103,8 +103,8 @@ def test_variable_with_reference(make_simulation, isolated_tax_benefit_system): class disposable_income(Variable): definition_period = DateUnit.MONTH - def formula(household, period): - return household.empty_array() + def formula(self, period): + return self.empty_array() isolated_tax_benefit_system.update_variable(disposable_income) simulation = make_simulation(isolated_tax_benefit_system, variables, PERIOD) @@ -114,13 +114,13 @@ def formula(household, period): assert result == 0 -def test_variable_name_conflict(tax_benefit_system): +def test_variable_name_conflict(tax_benefit_system) -> None: class disposable_income(Variable): reference = "disposable_income" definition_period = DateUnit.MONTH - def formula(household, period): - return household.empty_array() + def formula(self, period): + return self.empty_array() with pytest.raises(VariableNameConflictError): tax_benefit_system.add_variable(disposable_income) diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index 14886532c6..acb08c6424 100644 --- a/tests/core/test_cycles.py +++ b/tests/core/test_cycles.py @@ -25,8 +25,8 @@ class variable1(Variable): entity = entities.Person definition_period = DateUnit.MONTH - def formula(person, period): - return person("variable2", period) + def formula(self, period): + return self("variable2", period) class variable2(Variable): @@ -34,8 +34,8 @@ class variable2(Variable): entity = entities.Person definition_period = DateUnit.MONTH - def formula(person, period): - return person("variable1", period) + def formula(self, period): + return self("variable1", period) # 3 <--> 4 with a period offset @@ -44,8 +44,8 @@ class variable3(Variable): entity = entities.Person definition_period = DateUnit.MONTH - def formula(person, period): - return person("variable4", period.last_month) + def formula(self, period): + return self("variable4", period.last_month) class variable4(Variable): @@ -53,8 +53,8 @@ class variable4(Variable): entity = entities.Person definition_period = DateUnit.MONTH - def formula(person, period): - return person("variable3", period) + def formula(self, period): + return self("variable3", period) # 5 -f-> 6 with a period offset @@ -64,8 +64,8 @@ class variable5(Variable): entity = entities.Person definition_period = DateUnit.MONTH - def formula(person, period): - variable6 = person("variable6", period.last_month) + def formula(self, period): + variable6 = self("variable6", period.last_month) return 5 + variable6 @@ -74,8 +74,8 @@ class variable6(Variable): entity = entities.Person definition_period = DateUnit.MONTH - def formula(person, period): - variable5 = person("variable5", period) + def formula(self, period): + variable5 = self("variable5", period) return 6 + variable5 @@ -84,8 +84,8 @@ class variable7(Variable): entity = entities.Person definition_period = DateUnit.MONTH - def formula(person, period): - variable5 = person("variable5", period) + def formula(self, period): + variable5 = self("variable5", period) return 7 + variable5 @@ -95,15 +95,14 @@ class cotisation(Variable): entity = entities.Person definition_period = DateUnit.MONTH - def formula(person, period): + def formula(self, period): if period.start.month == 12: - return 2 * person("cotisation", period.last_month) - else: - return person.empty_array() + 1 + return 2 * self("cotisation", period.last_month) + return self.empty_array() + 1 @pytest.fixture(scope="module", autouse=True) -def add_variables_to_tax_benefit_system(tax_benefit_system): +def add_variables_to_tax_benefit_system(tax_benefit_system) -> None: tax_benefit_system.add_variables( variable1, variable2, @@ -116,34 +115,35 @@ def add_variables_to_tax_benefit_system(tax_benefit_system): ) -def test_pure_cycle(simulation, reference_period): +def test_pure_cycle(simulation, reference_period) -> None: with pytest.raises(CycleError): simulation.calculate("variable1", period=reference_period) -def test_spirals_result_in_default_value(simulation, reference_period): +def test_spirals_result_in_default_value(simulation, reference_period) -> None: variable3 = simulation.calculate("variable3", period=reference_period) tools.assert_near(variable3, [0]) -def test_spiral_heuristic(simulation, reference_period): +def test_spiral_heuristic(simulation, reference_period) -> None: variable5 = simulation.calculate("variable5", period=reference_period) variable6 = simulation.calculate("variable6", period=reference_period) variable6_last_month = simulation.calculate( - "variable6", reference_period.last_month + "variable6", + reference_period.last_month, ) tools.assert_near(variable5, [11]) tools.assert_near(variable6, [11]) tools.assert_near(variable6_last_month, [11]) -def test_spiral_cache(simulation, reference_period): +def test_spiral_cache(simulation, reference_period) -> None: simulation.calculate("variable7", period=reference_period) cached_variable7 = simulation.get_holder("variable7").get_array(reference_period) assert cached_variable7 is not None -def test_cotisation_1_level(simulation, reference_period): +def test_cotisation_1_level(simulation, reference_period) -> None: month = reference_period.last_month cotisation = simulation.calculate("cotisation", period=month) tools.assert_near(cotisation, [0]) diff --git a/tests/core/test_dump_restore.py b/tests/core/test_dump_restore.py index b03c55a831..c84044165c 100644 --- a/tests/core/test_dump_restore.py +++ b/tests/core/test_dump_restore.py @@ -9,10 +9,11 @@ from openfisca_core.tools import simulation_dumper -def test_dump(tax_benefit_system): +def test_dump(tax_benefit_system) -> None: directory = tempfile.mkdtemp(prefix="openfisca_") simulation = SimulationBuilder().build_from_entities( - tax_benefit_system, situation_examples.couple + tax_benefit_system, + situation_examples.couple, ) calculated_value = simulation.calculate("disposable_income", "2018-01") simulation_dumper.dump_simulation(simulation, directory) @@ -26,13 +27,16 @@ def test_dump(tax_benefit_system): testing.assert_array_equal(simulation.household.ids, simulation_2.household.ids) testing.assert_array_equal(simulation.household.count, simulation_2.household.count) testing.assert_array_equal( - simulation.household.members_position, simulation_2.household.members_position + simulation.household.members_position, + simulation_2.household.members_position, ) testing.assert_array_equal( - simulation.household.members_entity_id, simulation_2.household.members_entity_id + simulation.household.members_entity_id, + simulation_2.household.members_entity_id, ) testing.assert_array_equal( - simulation.household.members_role, simulation_2.household.members_role + simulation.household.members_role, + simulation_2.household.members_role, ) # Check calculated values are in cache diff --git a/tests/core/test_entities.py b/tests/core/test_entities.py index 1b7b646311..aba17dc4dc 100644 --- a/tests/core/test_entities.py +++ b/tests/core/test_entities.py @@ -34,7 +34,7 @@ def new_simulation(tax_benefit_system, test_case, period=MONTH): return simulation_builder.build_from_entities(tax_benefit_system, test_case) -def test_role_index_and_positions(tax_benefit_system): +def test_role_index_and_positions(tax_benefit_system) -> None: simulation = new_simulation(tax_benefit_system, TEST_CASE) tools.assert_near(simulation.household.members_entity_id, [0, 0, 0, 0, 1, 1]) assert ( @@ -46,7 +46,7 @@ def test_role_index_and_positions(tax_benefit_system): assert simulation.household.ids == ["h1", "h2"] -def test_entity_structure_with_constructor(tax_benefit_system): +def test_entity_structure_with_constructor(tax_benefit_system) -> None: simulation_yaml = """ persons: bill: {} @@ -68,7 +68,8 @@ def test_entity_structure_with_constructor(tax_benefit_system): """ simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, test_runner.yaml.safe_load(simulation_yaml) + tax_benefit_system, + test_runner.yaml.safe_load(simulation_yaml), ) household = simulation.household @@ -81,7 +82,7 @@ def test_entity_structure_with_constructor(tax_benefit_system): tools.assert_near(household.members_position, [0, 1, 0, 2, 3]) -def test_entity_variables_with_constructor(tax_benefit_system): +def test_entity_variables_with_constructor(tax_benefit_system) -> None: simulation_yaml = """ persons: bill: {} @@ -107,13 +108,14 @@ def test_entity_variables_with_constructor(tax_benefit_system): """ simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, test_runner.yaml.safe_load(simulation_yaml) + tax_benefit_system, + test_runner.yaml.safe_load(simulation_yaml), ) household = simulation.household tools.assert_near(household("rent", "2017-06"), [800, 600]) -def test_person_variable_with_constructor(tax_benefit_system): +def test_person_variable_with_constructor(tax_benefit_system) -> None: simulation_yaml = """ persons: bill: @@ -142,14 +144,15 @@ def test_person_variable_with_constructor(tax_benefit_system): """ simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, test_runner.yaml.safe_load(simulation_yaml) + tax_benefit_system, + test_runner.yaml.safe_load(simulation_yaml), ) person = simulation.person tools.assert_near(person("salary", "2017-11"), [1500, 0, 3000, 0, 0]) tools.assert_near(person("salary", "2017-12"), [2000, 0, 4000, 0, 0]) -def test_set_input_with_constructor(tax_benefit_system): +def test_set_input_with_constructor(tax_benefit_system) -> None: simulation_yaml = """ persons: bill: @@ -183,34 +186,38 @@ def test_set_input_with_constructor(tax_benefit_system): """ simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, test_runner.yaml.safe_load(simulation_yaml) + tax_benefit_system, + test_runner.yaml.safe_load(simulation_yaml), ) person = simulation.person tools.assert_near(person("salary", "2017-12"), [2000, 0, 4000, 0, 0]) tools.assert_near(person("salary", "2017-10"), [2000, 3000, 1600, 0, 0]) -def test_has_role(tax_benefit_system): +def test_has_role(tax_benefit_system) -> None: simulation = new_simulation(tax_benefit_system, TEST_CASE) individu = simulation.persons tools.assert_near(individu.has_role(CHILD), [False, False, True, True, False, True]) -def test_has_role_with_subrole(tax_benefit_system): +def test_has_role_with_subrole(tax_benefit_system) -> None: simulation = new_simulation(tax_benefit_system, TEST_CASE) individu = simulation.persons tools.assert_near( - individu.has_role(PARENT), [True, True, False, False, True, False] + individu.has_role(PARENT), + [True, True, False, False, True, False], ) tools.assert_near( - individu.has_role(FIRST_PARENT), [True, False, False, False, True, False] + individu.has_role(FIRST_PARENT), + [True, False, False, False, True, False], ) tools.assert_near( - individu.has_role(SECOND_PARENT), [False, True, False, False, False, False] + individu.has_role(SECOND_PARENT), + [False, True, False, False, False, False], ) -def test_project(tax_benefit_system): +def test_project(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE) test_case["households"]["h1"]["housing_tax"] = 20000 @@ -226,7 +233,7 @@ def test_project(tax_benefit_system): tools.assert_near(housing_tax_projected_on_parents, [20000, 20000, 0, 0, 0, 0]) -def test_implicit_projection(tax_benefit_system): +def test_implicit_projection(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE) test_case["households"]["h1"]["housing_tax"] = 20000 @@ -237,7 +244,7 @@ def test_implicit_projection(tax_benefit_system): tools.assert_near(housing_tax, [20000, 20000, 20000, 20000, 0, 0]) -def test_sum(tax_benefit_system): +def test_sum(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE) test_case["persons"]["ind0"]["salary"] = 1000 test_case["persons"]["ind1"]["salary"] = 1500 @@ -257,7 +264,7 @@ def test_sum(tax_benefit_system): tools.assert_near(total_salary_parents_by_household, [2500, 3000]) -def test_any(tax_benefit_system): +def test_any(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE_AGES) simulation = new_simulation(tax_benefit_system, test_case) household = simulation.household @@ -272,7 +279,7 @@ def test_any(tax_benefit_system): tools.assert_near(has_household_CHILD_with_age_sup_18, [False, True]) -def test_all(tax_benefit_system): +def test_all(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE_AGES) simulation = new_simulation(tax_benefit_system, test_case) household = simulation.household @@ -287,7 +294,7 @@ def test_all(tax_benefit_system): tools.assert_near(all_parents_age_sup_18, [True, True]) -def test_max(tax_benefit_system): +def test_max(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE_AGES) simulation = new_simulation(tax_benefit_system, test_case) household = simulation.household @@ -301,7 +308,7 @@ def test_max(tax_benefit_system): tools.assert_near(age_max_child, [9, 20]) -def test_min(tax_benefit_system): +def test_min(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE_AGES) simulation = new_simulation(tax_benefit_system, test_case) household = simulation.household @@ -315,7 +322,7 @@ def test_min(tax_benefit_system): tools.assert_near(age_min_parents, [37, 54]) -def test_value_nth_person(tax_benefit_system): +def test_value_nth_person(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE_AGES) simulation = new_simulation(tax_benefit_system, test_case) household = simulation.household @@ -334,7 +341,7 @@ def test_value_nth_person(tax_benefit_system): tools.assert_near(result3, [9, -1]) -def test_rank(tax_benefit_system): +def test_rank(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE_AGES) simulation = new_simulation(tax_benefit_system, test_case) person = simulation.person @@ -344,12 +351,14 @@ def test_rank(tax_benefit_system): tools.assert_near(rank, [3, 2, 0, 1, 1, 0]) rank_in_siblings = person.get_rank( - person.household, -age, condition=person.has_role(entities.Household.CHILD) + person.household, + -age, + condition=person.has_role(entities.Household.CHILD), ) tools.assert_near(rank_in_siblings, [-1, -1, 1, 0, -1, 0]) -def test_partner(tax_benefit_system): +def test_partner(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE) test_case["persons"]["ind0"]["salary"] = 1000 test_case["persons"]["ind1"]["salary"] = 1500 @@ -366,7 +375,7 @@ def test_partner(tax_benefit_system): tools.assert_near(salary_second_parent, [1500, 1000, 0, 0, 0, 0]) -def test_value_from_first_person(tax_benefit_system): +def test_value_from_first_person(tax_benefit_system) -> None: test_case = deepcopy(TEST_CASE) test_case["persons"]["ind0"]["salary"] = 1000 test_case["persons"]["ind1"]["salary"] = 1500 @@ -382,9 +391,10 @@ def test_value_from_first_person(tax_benefit_system): tools.assert_near(salary_first_person, [1000, 3000]) -def test_projectors_methods(tax_benefit_system): +def test_projectors_methods(tax_benefit_system) -> None: simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, situation_examples.couple + tax_benefit_system, + situation_examples.couple, ) household = simulation.household person = simulation.person @@ -403,7 +413,7 @@ def test_projectors_methods(tax_benefit_system): ) # Must be of a person dimension -def test_sum_following_bug_ipp_1(tax_benefit_system): +def test_sum_following_bug_ipp_1(tax_benefit_system) -> None: test_case = { "persons": {"ind0": {}, "ind1": {}, "ind2": {}, "ind3": {}}, "households": { @@ -425,7 +435,7 @@ def test_sum_following_bug_ipp_1(tax_benefit_system): tools.assert_near(nb_eligibles_by_household, [0, 2]) -def test_sum_following_bug_ipp_2(tax_benefit_system): +def test_sum_following_bug_ipp_2(tax_benefit_system) -> None: test_case = { "persons": {"ind0": {}, "ind1": {}, "ind2": {}, "ind3": {}}, "households": { @@ -447,7 +457,7 @@ def test_sum_following_bug_ipp_2(tax_benefit_system): tools.assert_near(nb_eligibles_by_household, [2, 0]) -def test_get_memory_usage(tax_benefit_system): +def test_get_memory_usage(tax_benefit_system) -> None: test_case = deepcopy(situation_examples.single) test_case["persons"]["Alicia"]["salary"] = {"2017-01": 0} simulation = SimulationBuilder().build_from_dict(tax_benefit_system, test_case) @@ -457,7 +467,7 @@ def test_get_memory_usage(tax_benefit_system): assert len(memory_usage["by_variable"]) == 1 -def test_unordered_persons(tax_benefit_system): +def test_unordered_persons(tax_benefit_system) -> None: test_case = { "persons": { "ind4": {}, @@ -527,11 +537,14 @@ def test_unordered_persons(tax_benefit_system): # Projection entity -> persons tools.assert_near( - household.project(accommodation_size), [60, 160, 160, 160, 60, 160] + household.project(accommodation_size), + [60, 160, 160, 160, 60, 160], ) tools.assert_near( - household.project(accommodation_size, role=PARENT), [60, 0, 160, 0, 0, 160] + household.project(accommodation_size, role=PARENT), + [60, 0, 160, 0, 0, 160], ) tools.assert_near( - household.project(accommodation_size, role=CHILD), [0, 160, 0, 160, 60, 0] + household.project(accommodation_size, role=CHILD), + [0, 160, 0, 160, 60, 0], ) diff --git a/tests/core/test_extensions.py b/tests/core/test_extensions.py index 2bb2689b15..4854815ac3 100644 --- a/tests/core/test_extensions.py +++ b/tests/core/test_extensions.py @@ -1,7 +1,7 @@ import pytest -def test_load_extension(tax_benefit_system): +def test_load_extension(tax_benefit_system) -> None: tbs = tax_benefit_system.clone() assert tbs.get_variable("local_town_child_allowance") is None @@ -11,7 +11,7 @@ def test_load_extension(tax_benefit_system): assert tax_benefit_system.get_variable("local_town_child_allowance") is None -def test_access_to_parameters(tax_benefit_system): +def test_access_to_parameters(tax_benefit_system) -> None: tbs = tax_benefit_system.clone() tbs.load_extension("openfisca_extension_template") @@ -19,6 +19,8 @@ def test_access_to_parameters(tax_benefit_system): assert tbs.parameters.local_town.child_allowance.amount("2016-01") == 100.0 -def test_failure_to_load_extension_when_directory_doesnt_exist(tax_benefit_system): +def test_failure_to_load_extension_when_directory_doesnt_exist( + tax_benefit_system, +) -> None: with pytest.raises(ValueError): tax_benefit_system.load_extension("/this/is/not/a/real/path") diff --git a/tests/core/test_formulas.py b/tests/core/test_formulas.py index c8a5379801..32e6fd35e7 100644 --- a/tests/core/test_formulas.py +++ b/tests/core/test_formulas.py @@ -21,10 +21,9 @@ class uses_multiplication(Variable): label = "Variable with formula that uses multiplication" definition_period = DateUnit.MONTH - def formula(person, period): - choice = person("choice", period) - result = (choice == 1) * 80 + (choice == 2) * 90 - return result + def formula(self, period): + choice = self("choice", period) + return (choice == 1) * 80 + (choice == 2) * 90 class returns_scalar(Variable): @@ -33,7 +32,7 @@ class returns_scalar(Variable): label = "Variable with formula that returns a scalar value" definition_period = DateUnit.MONTH - def formula(person, period): + def formula(self, period) -> int: return 666 @@ -43,27 +42,29 @@ class uses_switch(Variable): label = "Variable with formula that uses switch" definition_period = DateUnit.MONTH - def formula(person, period): - choice = person("choice", period) - result = commons.switch( + def formula(self, period): + choice = self("choice", period) + return commons.switch( choice, { 1: 80, 2: 90, }, ) - return result @fixture(scope="module", autouse=True) -def add_variables_to_tax_benefit_system(tax_benefit_system): +def add_variables_to_tax_benefit_system(tax_benefit_system) -> None: tax_benefit_system.add_variables( - choice, uses_multiplication, uses_switch, returns_scalar + choice, + uses_multiplication, + uses_switch, + returns_scalar, ) @fixture -def month(): +def month() -> str: return "2013-01" @@ -72,35 +73,36 @@ def simulation(tax_benefit_system, month): simulation_builder = SimulationBuilder() simulation_builder.default_period = month simulation = simulation_builder.build_from_variables( - tax_benefit_system, {"choice": numpy.random.randint(2, size=1000) + 1} + tax_benefit_system, + {"choice": numpy.random.randint(2, size=1000) + 1}, ) simulation.debug = True return simulation -def test_switch(simulation, month): +def test_switch(simulation, month) -> None: uses_switch = simulation.calculate("uses_switch", period=month) assert isinstance(uses_switch, numpy.ndarray) -def test_multiplication(simulation, month): +def test_multiplication(simulation, month) -> None: uses_multiplication = simulation.calculate("uses_multiplication", period=month) assert isinstance(uses_multiplication, numpy.ndarray) -def test_broadcast_scalar(simulation, month): +def test_broadcast_scalar(simulation, month) -> None: array_value = simulation.calculate("returns_scalar", period=month) assert isinstance(array_value, numpy.ndarray) assert array_value == approx(numpy.repeat(666, 1000)) -def test_compare_multiplication_and_switch(simulation, month): +def test_compare_multiplication_and_switch(simulation, month) -> None: uses_multiplication = simulation.calculate("uses_multiplication", period=month) uses_switch = simulation.calculate("uses_switch", period=month) assert numpy.all(uses_switch == uses_multiplication) -def test_group_encapsulation(): +def test_group_encapsulation() -> None: """Projects a calculation to all members of an entity. When a household contains more than one family @@ -128,7 +130,7 @@ def test_group_encapsulation(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) household_entity = build_entity( @@ -140,7 +142,7 @@ def test_group_encapsulation(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) @@ -158,8 +160,8 @@ class projected_family_level_variable(Variable): entity = family_entity definition_period = DateUnit.ETERNITY - def formula(family, period): - return family.household("household_level_variable", period) + def formula(self, period): + return self.household("household_level_variable", period) system.add_variables(household_level_variable, projected_family_level_variable) @@ -175,7 +177,7 @@ def formula(family, period): "household1": { "members": ["person1", "person2", "person3"], "household_level_variable": {"eternity": 5}, - } + }, }, }, ) diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index 088ca15935..c72d053ad6 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -15,51 +15,56 @@ @pytest.fixture def single(tax_benefit_system): return SimulationBuilder().build_from_entities( - tax_benefit_system, situation_examples.single + tax_benefit_system, + situation_examples.single, ) @pytest.fixture def couple(tax_benefit_system): return SimulationBuilder().build_from_entities( - tax_benefit_system, situation_examples.couple + tax_benefit_system, + situation_examples.couple, ) period = periods.period("2017-12") -def test_set_input_enum_string(couple): +def test_set_input_enum_string(couple) -> None: simulation = couple status_occupancy = numpy.asarray(["free_lodger"]) simulation.household.get_holder("housing_occupancy_status").set_input( - period, status_occupancy + period, + status_occupancy, ) result = simulation.calculate("housing_occupancy_status", period) assert result == housing.HousingOccupancyStatus.free_lodger -def test_set_input_enum_int(couple): +def test_set_input_enum_int(couple) -> None: simulation = couple status_occupancy = numpy.asarray([2], dtype=numpy.int16) simulation.household.get_holder("housing_occupancy_status").set_input( - period, status_occupancy + period, + status_occupancy, ) result = simulation.calculate("housing_occupancy_status", period) assert result == housing.HousingOccupancyStatus.free_lodger -def test_set_input_enum_item(couple): +def test_set_input_enum_item(couple) -> None: simulation = couple status_occupancy = numpy.asarray([housing.HousingOccupancyStatus.free_lodger]) simulation.household.get_holder("housing_occupancy_status").set_input( - period, status_occupancy + period, + status_occupancy, ) result = simulation.calculate("housing_occupancy_status", period) assert result == housing.HousingOccupancyStatus.free_lodger -def test_yearly_input_month_variable(couple): +def test_yearly_input_month_variable(couple) -> None: with pytest.raises(PeriodMismatchError) as error: couple.set_input("rent", 2019, 3000) assert ( @@ -68,7 +73,7 @@ def test_yearly_input_month_variable(couple): ) -def test_3_months_input_month_variable(couple): +def test_3_months_input_month_variable(couple) -> None: with pytest.raises(PeriodMismatchError) as error: couple.set_input("rent", "month:2019-01:3", 3000) assert ( @@ -77,7 +82,7 @@ def test_3_months_input_month_variable(couple): ) -def test_month_input_year_variable(couple): +def test_month_input_year_variable(couple) -> None: with pytest.raises(PeriodMismatchError) as error: couple.set_input("housing_tax", "2019-01", 3000) assert ( @@ -86,23 +91,24 @@ def test_month_input_year_variable(couple): ) -def test_enum_dtype(couple): +def test_enum_dtype(couple) -> None: simulation = couple status_occupancy = numpy.asarray([2], dtype=numpy.int16) simulation.household.get_holder("housing_occupancy_status").set_input( - period, status_occupancy + period, + status_occupancy, ) result = simulation.calculate("housing_occupancy_status", period) assert result.dtype.kind is not None -def test_permanent_variable_empty(single): +def test_permanent_variable_empty(single) -> None: simulation = single holder = simulation.person.get_holder("birth") assert holder.get_array(None) is None -def test_permanent_variable_filled(single): +def test_permanent_variable_filled(single) -> None: simulation = single holder = simulation.person.get_holder("birth") value = numpy.asarray(["1980-01-01"], dtype=holder.variable.dtype) @@ -112,7 +118,7 @@ def test_permanent_variable_filled(single): assert holder.get_array("2016-01") == value -def test_delete_arrays(single): +def test_delete_arrays(single) -> None: simulation = single salary_holder = simulation.person.get_holder("salary") salary_holder.set_input(periods.period(2017), numpy.asarray([30000])) @@ -131,7 +137,7 @@ def test_delete_arrays(single): assert simulation.person("salary", "2018-01") == 1250 -def test_get_memory_usage(single): +def test_get_memory_usage(single) -> None: simulation = single salary_holder = simulation.person.get_holder("salary") memory_usage = salary_holder.get_memory_usage() @@ -145,7 +151,7 @@ def test_get_memory_usage(single): assert memory_usage["total_nb_bytes"] == 4 * 12 * 1 -def test_get_memory_usage_with_trace(single): +def test_get_memory_usage_with_trace(single) -> None: simulation = single simulation.trace = True salary_holder = simulation.person.get_holder("salary") @@ -159,24 +165,24 @@ def test_get_memory_usage_with_trace(single): assert memory_usage["nb_requests_by_array"] == 1.25 # 15 calculations / 12 arrays -def test_set_input_dispatch_by_period(single): +def test_set_input_dispatch_by_period(single) -> None: simulation = single variable = simulation.tax_benefit_system.get_variable("housing_occupancy_status") entity = simulation.household holder = Holder(variable, entity) holders.set_input_dispatch_by_period(holder, periods.period(2019), "owner") assert holder.get_array("2019-01") == holder.get_array( - "2019-12" + "2019-12", ) # Check the feature assert holder.get_array("2019-01") is holder.get_array( - "2019-12" + "2019-12", ) # Check that the vectors are the same in memory, to avoid duplication force_storage_on_disk = MemoryConfig(max_memory_occupation=0) -def test_delete_arrays_on_disk(single): +def test_delete_arrays_on_disk(single) -> None: simulation = single simulation.memory_config = force_storage_on_disk salary_holder = simulation.person.get_holder("salary") @@ -190,7 +196,7 @@ def test_delete_arrays_on_disk(single): assert simulation.person("salary", "2018-01") == 1250 -def test_cache_disk(couple): +def test_cache_disk(couple) -> None: simulation = couple simulation.memory_config = force_storage_on_disk month = periods.period("2017-01") @@ -201,7 +207,7 @@ def test_cache_disk(couple): tools.assert_near(data, stored_data) -def test_known_periods(couple): +def test_known_periods(couple) -> None: simulation = couple simulation.memory_config = force_storage_on_disk month = periods.period("2017-01") @@ -214,20 +220,22 @@ def test_known_periods(couple): assert sorted(holder.get_known_periods()), [month == month_2] -def test_cache_enum_on_disk(single): +def test_cache_enum_on_disk(single) -> None: simulation = single simulation.memory_config = force_storage_on_disk month = periods.period("2017-01") simulation.calculate("housing_occupancy_status", month) # First calculation housing_occupancy_status = simulation.calculate( - "housing_occupancy_status", month + "housing_occupancy_status", + month, ) # Read from cache assert housing_occupancy_status == housing.HousingOccupancyStatus.tenant -def test_set_not_cached_variable(single): +def test_set_not_cached_variable(single) -> None: dont_cache_variable = MemoryConfig( - max_memory_occupation=1, variables_to_drop=["salary"] + max_memory_occupation=1, + variables_to_drop=["salary"], ) simulation = single simulation.memory_config = dont_cache_variable @@ -237,7 +245,7 @@ def test_set_not_cached_variable(single): assert simulation.calculate("salary", "2015-01") == array -def test_set_input_float_to_int(single): +def test_set_input_float_to_int(single) -> None: simulation = single age = numpy.asarray([50.6]) simulation.person.get_holder("age").set_input(period, age) diff --git a/tests/core/test_opt_out_cache.py b/tests/core/test_opt_out_cache.py index e9fe3a2469..2f61da2898 100644 --- a/tests/core/test_opt_out_cache.py +++ b/tests/core/test_opt_out_cache.py @@ -22,8 +22,8 @@ class intermediate(Variable): label = "Intermediate result that don't need to be cached" definition_period = DateUnit.MONTH - def formula(person, period): - return person("input", period) + def formula(self, period): + return self("input", period) class output(Variable): @@ -32,29 +32,29 @@ class output(Variable): label = "Output variable" definition_period = DateUnit.MONTH - def formula(person, period): - return person("intermediate", period) + def formula(self, period): + return self("intermediate", period) @pytest.fixture(scope="module", autouse=True) -def add_variables_to_tax_benefit_system(tax_benefit_system): +def add_variables_to_tax_benefit_system(tax_benefit_system) -> None: tax_benefit_system.add_variables(input, intermediate, output) @pytest.fixture(scope="module", autouse=True) -def add_variables_to_cache_blakclist(tax_benefit_system): - tax_benefit_system.cache_blacklist = set(["intermediate"]) +def add_variables_to_cache_blakclist(tax_benefit_system) -> None: + tax_benefit_system.cache_blacklist = {"intermediate"} @pytest.mark.parametrize("simulation", [({"input": 1}, PERIOD)], indirect=True) -def test_without_cache_opt_out(simulation): +def test_without_cache_opt_out(simulation) -> None: simulation.calculate("output", period=PERIOD) intermediate_cache = simulation.persons.get_holder("intermediate") assert intermediate_cache.get_array(PERIOD) is not None @pytest.mark.parametrize("simulation", [({"input": 1}, PERIOD)], indirect=True) -def test_with_cache_opt_out(simulation): +def test_with_cache_opt_out(simulation) -> None: simulation.debug = True simulation.opt_out_cache = True simulation.calculate("output", period=PERIOD) @@ -63,7 +63,7 @@ def test_with_cache_opt_out(simulation): @pytest.mark.parametrize("simulation", [({"input": 1}, PERIOD)], indirect=True) -def test_with_no_blacklist(simulation): +def test_with_no_blacklist(simulation) -> None: simulation.calculate("output", period=PERIOD) intermediate_cache = simulation.persons.get_holder("intermediate") assert intermediate_cache.get_array(PERIOD) is not None diff --git a/tests/core/test_parameters.py b/tests/core/test_parameters.py index 13e2874787..7fe63a8180 100644 --- a/tests/core/test_parameters.py +++ b/tests/core/test_parameters.py @@ -10,18 +10,19 @@ ) -def test_get_at_instant(tax_benefit_system): +def test_get_at_instant(tax_benefit_system) -> None: parameters = tax_benefit_system.parameters assert isinstance(parameters, ParameterNode), parameters parameters_at_instant = parameters("2016-01-01") assert isinstance( - parameters_at_instant, ParameterNodeAtInstant + parameters_at_instant, + ParameterNodeAtInstant, ), parameters_at_instant assert parameters_at_instant.taxes.income_tax_rate == 0.15 assert parameters_at_instant.benefits.basic_income == 600 -def test_param_values(tax_benefit_system): +def test_param_values(tax_benefit_system) -> None: dated_values = { "2015-01-01": 0.15, "2014-01-01": 0.14, @@ -36,47 +37,47 @@ def test_param_values(tax_benefit_system): ) -def test_param_before_it_is_defined(tax_benefit_system): +def test_param_before_it_is_defined(tax_benefit_system) -> None: with pytest.raises(ParameterNotFound): tax_benefit_system.get_parameters_at_instant("1997-12-31").taxes.income_tax_rate # The placeholder should have no effect on the parameter computation -def test_param_with_placeholder(tax_benefit_system): +def test_param_with_placeholder(tax_benefit_system) -> None: assert ( tax_benefit_system.get_parameters_at_instant("2018-01-01").taxes.income_tax_rate == 0.15 ) -def test_stopped_parameter_before_end_value(tax_benefit_system): +def test_stopped_parameter_before_end_value(tax_benefit_system) -> None: assert ( tax_benefit_system.get_parameters_at_instant( - "2011-12-31" + "2011-12-31", ).benefits.housing_allowance == 0.25 ) -def test_stopped_parameter_after_end_value(tax_benefit_system): +def test_stopped_parameter_after_end_value(tax_benefit_system) -> None: with pytest.raises(ParameterNotFound): tax_benefit_system.get_parameters_at_instant( - "2016-12-01" + "2016-12-01", ).benefits.housing_allowance -def test_parameter_for_period(tax_benefit_system): +def test_parameter_for_period(tax_benefit_system) -> None: income_tax_rate = tax_benefit_system.parameters.taxes.income_tax_rate assert income_tax_rate("2015") == income_tax_rate("2015-01-01") -def test_wrong_value(tax_benefit_system): +def test_wrong_value(tax_benefit_system) -> None: income_tax_rate = tax_benefit_system.parameters.taxes.income_tax_rate with pytest.raises(ValueError): income_tax_rate("test") -def test_parameter_repr(tax_benefit_system): +def test_parameter_repr(tax_benefit_system) -> None: parameters = tax_benefit_system.parameters tf = tempfile.NamedTemporaryFile(delete=False) tf.write(repr(parameters).encode("utf-8")) @@ -85,7 +86,7 @@ def test_parameter_repr(tax_benefit_system): assert repr(parameters) == repr(tf_parameters) -def test_parameters_metadata(tax_benefit_system): +def test_parameters_metadata(tax_benefit_system) -> None: parameter = tax_benefit_system.parameters.benefits.basic_income assert ( parameter.metadata["reference"] == "https://law.gov.example/basic-income/amount" @@ -101,7 +102,7 @@ def test_parameters_metadata(tax_benefit_system): assert scale.metadata["rate_unit"] == "/1" -def test_parameter_node_metadata(tax_benefit_system): +def test_parameter_node_metadata(tax_benefit_system) -> None: parameter = tax_benefit_system.parameters.benefits assert parameter.description == "Social benefits" @@ -109,7 +110,7 @@ def test_parameter_node_metadata(tax_benefit_system): assert parameter_2.description == "Housing tax" -def test_parameter_documentation(tax_benefit_system): +def test_parameter_documentation(tax_benefit_system) -> None: parameter = tax_benefit_system.parameters.benefits.housing_allowance assert ( parameter.documentation @@ -117,16 +118,16 @@ def test_parameter_documentation(tax_benefit_system): ) -def test_get_descendants(tax_benefit_system): +def test_get_descendants(tax_benefit_system) -> None: all_parameters = { parameter.name for parameter in tax_benefit_system.parameters.get_descendants() } assert all_parameters.issuperset( - {"taxes", "taxes.housing_tax", "taxes.housing_tax.minimal_amount"} + {"taxes", "taxes.housing_tax", "taxes.housing_tax.minimal_amount"}, ) -def test_name(): +def test_name() -> None: parameter_data = { "description": "Parameter indexed by a numeric key", "2010": {"values": {"2006-01-01": 0.0075}}, diff --git a/tests/core/test_projectors.py b/tests/core/test_projectors.py index 27391711c3..c62e49d3a7 100644 --- a/tests/core/test_projectors.py +++ b/tests/core/test_projectors.py @@ -1,4 +1,4 @@ -import numpy as np +import numpy from openfisca_core.entities import build_entity from openfisca_core.indexed_enums import Enum @@ -8,9 +8,8 @@ from openfisca_core.variables import Variable -def test_shortcut_to_containing_entity_provided(): - """ - Tests that, when an entity provides a containing entity, +def test_shortcut_to_containing_entity_provided() -> None: + """Tests that, when an entity provides a containing entity, the shortcut to that containing entity is provided. """ person_entity = build_entity( @@ -29,7 +28,7 @@ def test_shortcut_to_containing_entity_provided(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) household_entity = build_entity( @@ -41,7 +40,7 @@ def test_shortcut_to_containing_entity_provided(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) @@ -52,9 +51,8 @@ def test_shortcut_to_containing_entity_provided(): assert simulation.populations["family"].household.entity.key == "household" -def test_shortcut_to_containing_entity_not_provided(): - """ - Tests that, when an entity doesn't provide a containing +def test_shortcut_to_containing_entity_not_provided() -> None: + """Tests that, when an entity doesn't provide a containing entity, the shortcut to that containing entity is not provided. """ person_entity = build_entity( @@ -73,7 +71,7 @@ def test_shortcut_to_containing_entity_not_provided(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) household_entity = build_entity( @@ -85,7 +83,7 @@ def test_shortcut_to_containing_entity_not_provided(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) @@ -95,17 +93,15 @@ def test_shortcut_to_containing_entity_not_provided(): simulation = SimulationBuilder().build_from_dict(system, {}) try: simulation.populations["family"].household - raise AssertionError() + raise AssertionError except AttributeError: pass -def test_enum_projects_downwards(): - """ - Test that an Enum-type household-level variable projects +def test_enum_projects_downwards() -> None: + """Test that an Enum-type household-level variable projects values onto its members correctly. """ - person = build_entity( key="person", plural="people", @@ -121,7 +117,7 @@ def test_enum_projects_downwards(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) @@ -147,8 +143,8 @@ class projected_enum_variable(Variable): entity = person definition_period = DateUnit.ETERNITY - def formula(person, period): - return person.household("household_enum_variable", period) + def formula(self, period): + return self.household("household_enum_variable", period) system.add_variables(household_enum_variable, projected_enum_variable) @@ -160,23 +156,21 @@ def formula(person, period): "household1": { "members": ["person1", "person2", "person3"], "household_enum_variable": {"eternity": "SECOND_OPTION"}, - } + }, }, }, ) assert ( simulation.calculate("projected_enum_variable", "2021-01-01").decode_to_str() - == np.array(["SECOND_OPTION"] * 3) + == numpy.array(["SECOND_OPTION"] * 3) ).all() -def test_enum_projects_upwards(): - """ - Test that an Enum-type person-level variable projects +def test_enum_projects_upwards() -> None: + """Test that an Enum-type person-level variable projects values onto its household (from the first person) correctly. """ - person = build_entity( key="person", plural="people", @@ -192,7 +186,7 @@ def test_enum_projects_upwards(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) @@ -211,9 +205,9 @@ class household_projected_variable(Variable): entity = household definition_period = DateUnit.ETERNITY - def formula(household, period): - return household.value_from_first_person( - household.members("person_enum_variable", period) + def formula(self, period): + return self.value_from_first_person( + self.members("person_enum_variable", period), ) class person_enum_variable(Variable): @@ -236,25 +230,24 @@ class person_enum_variable(Variable): "households": { "household1": { "members": ["person1", "person2", "person3"], - } + }, }, }, ) assert ( simulation.calculate( - "household_projected_variable", "2021-01-01" + "household_projected_variable", + "2021-01-01", ).decode_to_str() - == np.array(["SECOND_OPTION"]) + == numpy.array(["SECOND_OPTION"]) ).all() -def test_enum_projects_between_containing_groups(): - """ - Test that an Enum-type person-level variable projects +def test_enum_projects_between_containing_groups() -> None: + """Test that an Enum-type person-level variable projects values onto its household (from the first person) correctly. """ - person_entity = build_entity( key="person", plural="people", @@ -271,7 +264,7 @@ def test_enum_projects_between_containing_groups(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) household_entity = build_entity( @@ -283,7 +276,7 @@ def test_enum_projects_between_containing_groups(): "key": "member", "plural": "members", "label": "Member", - } + }, ], ) @@ -309,16 +302,16 @@ class projected_family_level_variable(Variable): entity = family_entity definition_period = DateUnit.ETERNITY - def formula(family, period): - return family.household("household_level_variable", period) + def formula(self, period): + return self.household("household_level_variable", period) class decoded_projected_family_level_variable(Variable): value_type = str entity = family_entity definition_period = DateUnit.ETERNITY - def formula(family, period): - return family.household("household_level_variable", period).decode_to_str() + def formula(self, period): + return self.household("household_level_variable", period).decode_to_str() system.add_variables( household_level_variable, @@ -338,18 +331,19 @@ def formula(family, period): "household1": { "members": ["person1", "person2", "person3"], "household_level_variable": {"eternity": "SECOND_OPTION"}, - } + }, }, }, ) assert ( simulation.calculate( - "projected_family_level_variable", "2021-01-01" + "projected_family_level_variable", + "2021-01-01", ).decode_to_str() - == np.array(["SECOND_OPTION"]) + == numpy.array(["SECOND_OPTION"]) ).all() assert ( simulation.calculate("decoded_projected_family_level_variable", "2021-01-01") - == np.array(["SECOND_OPTION"]) + == numpy.array(["SECOND_OPTION"]) ).all() diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index 0c17bb1169..1f31bcde2a 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -21,16 +21,16 @@ class goes_to_school(Variable): class WithBasicIncomeNeutralized(Reform): - def apply(self): + def apply(self) -> None: self.neutralize_variable("basic_income") @pytest.fixture(scope="module", autouse=True) -def add_variables_to_tax_benefit_system(tax_benefit_system): +def add_variables_to_tax_benefit_system(tax_benefit_system) -> None: tax_benefit_system.add_variables(goes_to_school) -def test_formula_neutralization(make_simulation, tax_benefit_system): +def test_formula_neutralization(make_simulation, tax_benefit_system) -> None: reform = WithBasicIncomeNeutralized(tax_benefit_system) period = "2017-01" @@ -48,16 +48,18 @@ def test_formula_neutralization(make_simulation, tax_benefit_system): basic_income_reform = reform_simulation.calculate("basic_income", period="2013-01") assert_near(basic_income_reform, 0, absolute_error_margin=0) disposable_income_reform = reform_simulation.calculate( - "disposable_income", period=period + "disposable_income", + period=period, ) assert_near(disposable_income_reform, 0) def test_neutralization_variable_with_default_value( - make_simulation, tax_benefit_system -): + make_simulation, + tax_benefit_system, +) -> None: class test_goes_to_school_neutralization(Reform): - def apply(self): + def apply(self) -> None: self.neutralize_variable("goes_to_school") reform = test_goes_to_school_neutralization(tax_benefit_system) @@ -69,7 +71,7 @@ def apply(self): assert_near(goes_to_school, [True], absolute_error_margin=0) -def test_neutralization_optimization(make_simulation, tax_benefit_system): +def test_neutralization_optimization(make_simulation, tax_benefit_system) -> None: reform = WithBasicIncomeNeutralized(tax_benefit_system) period = "2017-01" @@ -84,9 +86,9 @@ def test_neutralization_optimization(make_simulation, tax_benefit_system): assert basic_income_holder.get_known_periods() == [] -def test_input_variable_neutralization(make_simulation, tax_benefit_system): +def test_input_variable_neutralization(make_simulation, tax_benefit_system) -> None: class test_salary_neutralization(Reform): - def apply(self): + def apply(self) -> None: self.neutralize_variable("salary") reform = test_salary_neutralization(tax_benefit_system) @@ -107,21 +109,24 @@ def apply(self): [0, 0], ) disposable_income_reform = reform_simulation.calculate( - "disposable_income", period=period + "disposable_income", + period=period, ) assert_near(disposable_income_reform, [600, 600]) -def test_permanent_variable_neutralization(make_simulation, tax_benefit_system): +def test_permanent_variable_neutralization(make_simulation, tax_benefit_system) -> None: class test_date_naissance_neutralization(Reform): - def apply(self): + def apply(self) -> None: self.neutralize_variable("birth") reform = test_date_naissance_neutralization(tax_benefit_system) period = "2017-01" simulation = make_simulation( - reform.base_tax_benefit_system, {"birth": "1980-01-01"}, period + reform.base_tax_benefit_system, + {"birth": "1980-01-01"}, + period, ) with warnings.catch_warnings(record=True) as raised_warnings: reform_simulation = make_simulation(reform, {"birth": "1980-01-01"}, period) @@ -133,25 +138,35 @@ def apply(self): assert str(reform_simulation.calculate("birth", None)[0]) == "1970-01-01" -def test_update_items(): +def test_update_items() -> None: def check_update_items( - description, value_history, start_instant, stop_instant, value, expected_items - ): + description, + value_history, + start_instant, + stop_instant, + value, + expected_items, + ) -> None: value_history.update( - period=None, start=start_instant, stop=stop_instant, value=value + period=None, + start=start_instant, + stop=stop_instant, + value=value, ) assert value_history == expected_items check_update_items( "Replace an item by a new item", ValuesHistory( - "dummy_name", {"2013-01-01": {"value": 0.0}, "2014-01-01": {"value": None}} + "dummy_name", + {"2013-01-01": {"value": 0.0}, "2014-01-01": {"value": None}}, ), periods.period(2013).start, periods.period(2013).stop, 1.0, ValuesHistory( - "dummy_name", {"2013-01-01": {"value": 1.0}, "2014-01-01": {"value": None}} + "dummy_name", + {"2013-01-01": {"value": 1.0}, "2014-01-01": {"value": None}}, ), ) check_update_items( @@ -179,7 +194,8 @@ def check_update_items( check_update_items( "Open the stop instant to the future", ValuesHistory( - "dummy_name", {"2013-01-01": {"value": 0.0}, "2014-01-01": {"value": None}} + "dummy_name", + {"2013-01-01": {"value": 0.0}, "2014-01-01": {"value": None}}, ), periods.period(2013).start, None, # stop instant @@ -189,7 +205,8 @@ def check_update_items( check_update_items( "Insert a new item in the middle of an existing item", ValuesHistory( - "dummy_name", {"2010-01-01": {"value": 0.0}, "2014-01-01": {"value": None}} + "dummy_name", + {"2010-01-01": {"value": 0.0}, "2014-01-01": {"value": None}}, ), periods.period(2011).start, periods.period(2011).stop, @@ -250,7 +267,8 @@ def check_update_items( None, # stop instant 1.0, ValuesHistory( - "dummy_name", {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 1.0}} + "dummy_name", + {"2006-01-01": {"value": 0.055}, "2014-01-01": {"value": 1.0}}, ), ) check_update_items( @@ -314,18 +332,18 @@ def check_update_items( ) -def test_add_variable(make_simulation, tax_benefit_system): +def test_add_variable(make_simulation, tax_benefit_system) -> None: class new_variable(Variable): value_type = int label = "Nouvelle variable introduite par la réforme" entity = Household definition_period = DateUnit.MONTH - def formula(household, period): - return household.empty_array() + 10 + def formula(self, period): + return self.empty_array() + 10 class test_add_variable(Reform): - def apply(self): + def apply(self) -> None: self.add_variable(new_variable) reform = test_add_variable(tax_benefit_system) @@ -337,21 +355,21 @@ def apply(self): assert_near(new_variable1, 10, absolute_error_margin=0) -def test_add_dated_variable(make_simulation, tax_benefit_system): +def test_add_dated_variable(make_simulation, tax_benefit_system) -> None: class new_dated_variable(Variable): value_type = int label = "Nouvelle variable introduite par la réforme" entity = Household definition_period = DateUnit.MONTH - def formula_2010_01_01(household, period): - return household.empty_array() + 10 + def formula_2010_01_01(self, period): + return self.empty_array() + 10 - def formula_2011_01_01(household, period): - return household.empty_array() + 15 + def formula_2011_01_01(self, period): + return self.empty_array() + 15 class test_add_variable(Reform): - def apply(self): + def apply(self) -> None: self.add_variable(new_dated_variable) reform = test_add_variable(tax_benefit_system) @@ -359,20 +377,21 @@ def apply(self): reform_simulation = make_simulation(reform, {}, "2013-01") reform_simulation.debug = True new_dated_variable1 = reform_simulation.calculate( - "new_dated_variable", period="2013-01" + "new_dated_variable", + period="2013-01", ) assert_near(new_dated_variable1, 15, absolute_error_margin=0) -def test_update_variable(make_simulation, tax_benefit_system): +def test_update_variable(make_simulation, tax_benefit_system) -> None: class disposable_income(Variable): definition_period = DateUnit.MONTH - def formula_2018(household, period): - return household.empty_array() + 10 + def formula_2018(self, period): + return self.empty_array() + 10 class test_update_variable(Reform): - def apply(self): + def apply(self) -> None: self.update_variable(disposable_income) reform = test_update_variable(tax_benefit_system) @@ -390,29 +409,31 @@ def apply(self): reform_simulation = make_simulation(reform, {}, 2018) disposable_income1 = reform_simulation.calculate( - "disposable_income", period="2018-01" + "disposable_income", + period="2018-01", ) assert_near(disposable_income1, 10, absolute_error_margin=0) disposable_income2 = reform_simulation.calculate( - "disposable_income", period="2017-01" + "disposable_income", + period="2017-01", ) # Before 2018, the former formula is used assert disposable_income2 > 100 -def test_replace_variable(tax_benefit_system): +def test_replace_variable(tax_benefit_system) -> None: class disposable_income(Variable): definition_period = DateUnit.MONTH entity = Person label = "Disposable income" value_type = float - def formula_2018(household, period): - return household.empty_array() + 10 + def formula_2018(self, period): + return self.empty_array() + 10 class test_update_variable(Reform): - def apply(self): + def apply(self) -> None: self.replace_variable(disposable_income) reform = test_update_variable(tax_benefit_system) @@ -421,7 +442,7 @@ def apply(self): assert disposable_income_reform.get_formula("2017") is None -def test_wrong_reform(tax_benefit_system): +def test_wrong_reform(tax_benefit_system) -> None: class wrong_reform(Reform): # A Reform must implement an `apply` method pass @@ -430,7 +451,7 @@ class wrong_reform(Reform): wrong_reform(tax_benefit_system) -def test_modify_parameters(tax_benefit_system): +def test_modify_parameters(tax_benefit_system) -> None: def modify_parameters(reference_parameters): reform_parameters_subtree = ParameterNode( "new_node", @@ -439,7 +460,7 @@ def modify_parameters(reference_parameters): "values": { "2000-01-01": {"value": True}, "2015-01-01": {"value": None}, - } + }, }, }, ) @@ -447,7 +468,7 @@ def modify_parameters(reference_parameters): return reference_parameters class test_modify_parameters(Reform): - def apply(self): + def apply(self) -> None: self.modify_parameters(modifier_function=modify_parameters) reform = test_modify_parameters(tax_benefit_system) @@ -460,7 +481,7 @@ def apply(self): assert parameters_at_instant.new_node.new_param is True -def test_attributes_conservation(tax_benefit_system): +def test_attributes_conservation(tax_benefit_system) -> None: class some_variable(Variable): value_type = int entity = Person @@ -475,7 +496,7 @@ class reform(Reform): class some_variable(Variable): default_value = 10 - def apply(self): + def apply(self) -> None: self.update_variable(some_variable) reformed_tbs = reform(tax_benefit_system) @@ -489,9 +510,9 @@ def apply(self): assert reform_variable.calculate_output == baseline_variable.calculate_output -def test_formulas_removal(tax_benefit_system): +def test_formulas_removal(tax_benefit_system) -> None: class reform(Reform): - def apply(self): + def apply(self) -> None: class basic_income(Variable): pass diff --git a/tests/core/test_simulation_builder.py b/tests/core/test_simulation_builder.py index d1dc0cde75..ec131920e2 100644 --- a/tests/core/test_simulation_builder.py +++ b/tests/core/test_simulation_builder.py @@ -1,4 +1,4 @@ -from typing import Iterable +from typing import TYPE_CHECKING import datetime @@ -15,6 +15,9 @@ from openfisca_core.tools import test_runner from openfisca_core.variables import Variable +if TYPE_CHECKING: + from collections.abc import Iterable + @pytest.fixture def int_variable(persons): @@ -23,7 +26,7 @@ class intvar(Variable): value_type = int entity = persons - def __init__(self): + def __init__(self) -> None: super().__init__() return intvar() @@ -36,7 +39,7 @@ class datevar(Variable): value_type = datetime.date entity = persons - def __init__(self): + def __init__(self) -> None: super().__init__() return datevar() @@ -54,15 +57,16 @@ class TestEnum(Variable): possible_values = Enum("foo", "bar") name = "enum" - def __init__(self): + def __init__(self) -> None: pass return TestEnum() -def test_build_default_simulation(tax_benefit_system): +def test_build_default_simulation(tax_benefit_system) -> None: one_person_simulation = SimulationBuilder().build_default_simulation( - tax_benefit_system, 1 + tax_benefit_system, + 1, ) assert one_person_simulation.persons.count == 1 assert one_person_simulation.household.count == 1 @@ -72,7 +76,8 @@ def test_build_default_simulation(tax_benefit_system): ) several_persons_simulation = SimulationBuilder().build_default_simulation( - tax_benefit_system, 4 + tax_benefit_system, + 4, ) assert several_persons_simulation.persons.count == 4 assert several_persons_simulation.household.count == 4 @@ -85,7 +90,7 @@ def test_build_default_simulation(tax_benefit_system): ).all() -def test_explicit_singular_entities(tax_benefit_system): +def test_explicit_singular_entities(tax_benefit_system) -> None: assert SimulationBuilder().explicit_singular_entities( tax_benefit_system, {"persons": {"Javier": {}}, "household": {"parents": ["Javier"]}}, @@ -95,7 +100,7 @@ def test_explicit_singular_entities(tax_benefit_system): } -def test_add_person_entity(persons): +def test_add_person_entity(persons) -> None: persons_json = {"Alicia": {"salary": {}}, "Javier": {}} simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, persons_json) @@ -103,7 +108,7 @@ def test_add_person_entity(persons): assert simulation_builder.get_ids("persons") == ["Alicia", "Javier"] -def test_numeric_ids(persons): +def test_numeric_ids(persons) -> None: persons_json = {1: {"salary": {}}, 2: {}} simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, persons_json) @@ -111,14 +116,14 @@ def test_numeric_ids(persons): assert simulation_builder.get_ids("persons") == ["1", "2"] -def test_add_person_entity_with_values(persons): +def test_add_person_entity_with_values(persons) -> None: persons_json = {"Alicia": {"salary": {"2018-11": 3000}}, "Javier": {}} simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, persons_json) tools.assert_near(simulation_builder.get_input("salary", "2018-11"), [3000, 0]) -def test_add_person_values_with_default_period(persons): +def test_add_person_values_with_default_period(persons) -> None: persons_json = {"Alicia": {"salary": 3000}, "Javier": {}} simulation_builder = SimulationBuilder() simulation_builder.set_default_period("2018-11") @@ -126,7 +131,7 @@ def test_add_person_values_with_default_period(persons): tools.assert_near(simulation_builder.get_input("salary", "2018-11"), [3000, 0]) -def test_add_person_values_with_default_period_old_syntax(persons): +def test_add_person_values_with_default_period_old_syntax(persons) -> None: persons_json = {"Alicia": {"salary": 3000}, "Javier": {}} simulation_builder = SimulationBuilder() simulation_builder.set_default_period("month:2018-11") @@ -134,7 +139,7 @@ def test_add_person_values_with_default_period_old_syntax(persons): tools.assert_near(simulation_builder.get_input("salary", "2018-11"), [3000, 0]) -def test_add_group_entity(households): +def test_add_group_entity(households) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_group_entity( "persons", @@ -156,7 +161,7 @@ def test_add_group_entity(households): ] -def test_add_group_entity_loose_syntax(households): +def test_add_group_entity_loose_syntax(households) -> None: simulation_builder = SimulationBuilder() simulation_builder.add_group_entity( "persons", @@ -178,71 +183,91 @@ def test_add_group_entity_loose_syntax(households): ] -def test_add_variable_value(persons): +def test_add_variable_value(persons) -> None: salary = persons.get_variable("salary") instance_index = 0 simulation_builder = SimulationBuilder() simulation_builder.entity_counts["persons"] = 1 simulation_builder.add_variable_value( - persons, salary, instance_index, "Alicia", "2018-11", 3000 + persons, + salary, + instance_index, + "Alicia", + "2018-11", + 3000, ) input_array = simulation_builder.get_input("salary", "2018-11") assert input_array[instance_index] == pytest.approx(3000) -def test_add_variable_value_as_expression(persons): +def test_add_variable_value_as_expression(persons) -> None: salary = persons.get_variable("salary") instance_index = 0 simulation_builder = SimulationBuilder() simulation_builder.entity_counts["persons"] = 1 simulation_builder.add_variable_value( - persons, salary, instance_index, "Alicia", "2018-11", "3 * 1000" + persons, + salary, + instance_index, + "Alicia", + "2018-11", + "3 * 1000", ) input_array = simulation_builder.get_input("salary", "2018-11") assert input_array[instance_index] == pytest.approx(3000) -def test_fail_on_wrong_data(persons): +def test_fail_on_wrong_data(persons) -> None: salary = persons.get_variable("salary") instance_index = 0 simulation_builder = SimulationBuilder() simulation_builder.entity_counts["persons"] = 1 with pytest.raises(SituationParsingError) as excinfo: simulation_builder.add_variable_value( - persons, salary, instance_index, "Alicia", "2018-11", "alicia" + persons, + salary, + instance_index, + "Alicia", + "2018-11", + "alicia", ) assert excinfo.value.error == { "persons": { "Alicia": { "salary": { - "2018-11": "Can't deal with value: expected type number, received 'alicia'." - } - } - } + "2018-11": "Can't deal with value: expected type number, received 'alicia'.", + }, + }, + }, } -def test_fail_on_ill_formed_expression(persons): +def test_fail_on_ill_formed_expression(persons) -> None: salary = persons.get_variable("salary") instance_index = 0 simulation_builder = SimulationBuilder() simulation_builder.entity_counts["persons"] = 1 with pytest.raises(SituationParsingError) as excinfo: simulation_builder.add_variable_value( - persons, salary, instance_index, "Alicia", "2018-11", "2 * / 1000" + persons, + salary, + instance_index, + "Alicia", + "2018-11", + "2 * / 1000", ) assert excinfo.value.error == { "persons": { "Alicia": { "salary": { - "2018-11": "I couldn't understand '2 * / 1000' as a value for 'salary'" - } - } - } + "2018-11": "I couldn't understand '2 * / 1000' as a value for 'salary'", + }, + }, + }, } -def test_fail_on_integer_overflow(persons, int_variable): +def test_fail_on_integer_overflow(persons, int_variable) -> None: instance_index = 0 simulation_builder = SimulationBuilder() simulation_builder.entity_counts["persons"] = 1 @@ -259,39 +284,49 @@ def test_fail_on_integer_overflow(persons, int_variable): "persons": { "Alicia": { "intvar": { - "2018-11": "Can't deal with value: '9223372036854775808', it's too large for type 'integer'." - } - } - } + "2018-11": "Can't deal with value: '9223372036854775808', it's too large for type 'integer'.", + }, + }, + }, } -def test_fail_on_date_parsing(persons, date_variable): +def test_fail_on_date_parsing(persons, date_variable) -> None: instance_index = 0 simulation_builder = SimulationBuilder() simulation_builder.entity_counts["persons"] = 1 with pytest.raises(SituationParsingError) as excinfo: simulation_builder.add_variable_value( - persons, date_variable, instance_index, "Alicia", "2018-11", "2019-02-30" + persons, + date_variable, + instance_index, + "Alicia", + "2018-11", + "2019-02-30", ) assert excinfo.value.error == { "persons": { - "Alicia": {"datevar": {"2018-11": "Can't deal with date: '2019-02-30'."}} - } + "Alicia": {"datevar": {"2018-11": "Can't deal with date: '2019-02-30'."}}, + }, } -def test_add_unknown_enum_variable_value(persons, enum_variable): +def test_add_unknown_enum_variable_value(persons, enum_variable) -> None: instance_index = 0 simulation_builder = SimulationBuilder() simulation_builder.entity_counts["persons"] = 1 with pytest.raises(SituationParsingError): simulation_builder.add_variable_value( - persons, enum_variable, instance_index, "Alicia", "2018-11", "baz" + persons, + enum_variable, + instance_index, + "Alicia", + "2018-11", + "baz", ) -def test_finalize_person_entity(persons): +def test_finalize_person_entity(persons) -> None: persons_json = {"Alicia": {"salary": {"2018-11": 3000}}, "Javier": {}} simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, persons_json) @@ -302,7 +337,7 @@ def test_finalize_person_entity(persons): assert population.ids == ["Alicia", "Javier"] -def test_canonicalize_period_keys(persons): +def test_canonicalize_period_keys(persons) -> None: persons_json = {"Alicia": {"salary": {"year:2018-01": 100}}} simulation_builder = SimulationBuilder() simulation_builder.add_person_entity(persons, persons_json) @@ -311,9 +346,10 @@ def test_canonicalize_period_keys(persons): tools.assert_near(population.get_holder("salary").get_array("2018-12"), [100]) -def test_finalize_households(tax_benefit_system): +def test_finalize_households(tax_benefit_system) -> None: simulation = Simulation( - tax_benefit_system, tax_benefit_system.instantiate_entities() + tax_benefit_system, + tax_benefit_system.instantiate_entities(), ) simulation_builder = SimulationBuilder() simulation_builder.add_group_entity( @@ -333,7 +369,7 @@ def test_finalize_households(tax_benefit_system): ) -def test_check_persons_to_allocate(): +def test_check_persons_to_allocate() -> None: entity_plural = "familles" persons_plural = "individus" person_id = "Alicia" @@ -354,7 +390,7 @@ def test_check_persons_to_allocate(): ) -def test_allocate_undeclared_person(): +def test_allocate_undeclared_person() -> None: entity_plural = "familles" persons_plural = "individus" person_id = "Alicia" @@ -377,13 +413,13 @@ def test_allocate_undeclared_person(): assert exception.value.error == { "familles": { "famille1": { - "parents": "Unexpected value: Alicia. Alicia has been declared in famille1 parents, but has not been declared in individus." - } - } + "parents": "Unexpected value: Alicia. Alicia has been declared in famille1 parents, but has not been declared in individus.", + }, + }, } -def test_allocate_person_twice(): +def test_allocate_person_twice() -> None: entity_plural = "familles" persons_plural = "individus" person_id = "Alicia" @@ -406,37 +442,39 @@ def test_allocate_person_twice(): assert exception.value.error == { "familles": { "famille1": { - "parents": "Alicia has been declared more than once in familles" - } - } + "parents": "Alicia has been declared more than once in familles", + }, + }, } -def test_one_person_without_household(tax_benefit_system): +def test_one_person_without_household(tax_benefit_system) -> None: simulation_dict = {"persons": {"Alicia": {}}} simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, simulation_dict + tax_benefit_system, + simulation_dict, ) assert simulation.household.count == 1 parents_in_households = simulation.household.nb_persons( - role=entities.Household.PARENT + role=entities.Household.PARENT, ) assert parents_in_households.tolist() == [ - 1 + 1, ] # household member default role is first_parent -def test_some_person_without_household(tax_benefit_system): +def test_some_person_without_household(tax_benefit_system) -> None: input_yaml = """ persons: {'Alicia': {}, 'Bob': {}} household: {'parents': ['Alicia']} """ simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, test_runner.yaml.safe_load(input_yaml) + tax_benefit_system, + test_runner.yaml.safe_load(input_yaml), ) assert simulation.household.count == 2 parents_in_households = simulation.household.nb_persons( - role=entities.Household.PARENT + role=entities.Household.PARENT, ) assert parents_in_households.tolist() == [ 1, @@ -444,7 +482,7 @@ def test_some_person_without_household(tax_benefit_system): ] # household member default role is first_parent -def test_nb_persons_in_households(tax_benefit_system): +def test_nb_persons_in_households(tax_benefit_system) -> None: persons_ids: Iterable = [2, 0, 1, 4, 3] households_ids: Iterable = ["c", "a", "b"] persons_households: Iterable = ["c", "a", "a", "b", "a"] @@ -454,7 +492,9 @@ def test_nb_persons_in_households(tax_benefit_system): simulation_builder.declare_person_entity("person", persons_ids) household_instance = simulation_builder.declare_entity("household", households_ids) simulation_builder.join_with_persons( - household_instance, persons_households, ["first_parent"] * 5 + household_instance, + persons_households, + ["first_parent"] * 5, ) persons_in_households = simulation_builder.nb_persons("household") @@ -462,7 +502,7 @@ def test_nb_persons_in_households(tax_benefit_system): assert persons_in_households.tolist() == [1, 3, 1] -def test_nb_persons_no_role(tax_benefit_system): +def test_nb_persons_no_role(tax_benefit_system) -> None: persons_ids: Iterable = [2, 0, 1, 4, 3] households_ids: Iterable = ["c", "a", "b"] persons_households: Iterable = ["c", "a", "a", "b", "a"] @@ -473,10 +513,12 @@ def test_nb_persons_no_role(tax_benefit_system): household_instance = simulation_builder.declare_entity("household", households_ids) simulation_builder.join_with_persons( - household_instance, persons_households, ["first_parent"] * 5 + household_instance, + persons_households, + ["first_parent"] * 5, ) parents_in_households = household_instance.nb_persons( - role=entities.Household.PARENT + role=entities.Household.PARENT, ) assert parents_in_households.tolist() == [ @@ -486,7 +528,7 @@ def test_nb_persons_no_role(tax_benefit_system): ] # household member default role is first_parent -def test_nb_persons_by_role(tax_benefit_system): +def test_nb_persons_by_role(tax_benefit_system) -> None: persons_ids: Iterable = [2, 0, 1, 4, 3] households_ids: Iterable = ["c", "a", "b"] persons_households: Iterable = ["c", "a", "a", "b", "a"] @@ -504,16 +546,18 @@ def test_nb_persons_by_role(tax_benefit_system): household_instance = simulation_builder.declare_entity("household", households_ids) simulation_builder.join_with_persons( - household_instance, persons_households, persons_households_roles + household_instance, + persons_households, + persons_households_roles, ) parents_in_households = household_instance.nb_persons( - role=entities.Household.FIRST_PARENT + role=entities.Household.FIRST_PARENT, ) assert parents_in_households.tolist() == [0, 1, 1] -def test_integral_roles(tax_benefit_system): +def test_integral_roles(tax_benefit_system) -> None: persons_ids: Iterable = [2, 0, 1, 4, 3] households_ids: Iterable = ["c", "a", "b"] persons_households: Iterable = ["c", "a", "a", "b", "a"] @@ -526,10 +570,12 @@ def test_integral_roles(tax_benefit_system): household_instance = simulation_builder.declare_entity("household", households_ids) simulation_builder.join_with_persons( - household_instance, persons_households, persons_households_roles + household_instance, + persons_households, + persons_households_roles, ) parents_in_households = household_instance.nb_persons( - role=entities.Household.FIRST_PARENT + role=entities.Household.FIRST_PARENT, ) assert parents_in_households.tolist() == [0, 1, 1] @@ -538,7 +584,7 @@ def test_integral_roles(tax_benefit_system): # Test Intégration -def test_from_person_variable_to_group(tax_benefit_system): +def test_from_person_variable_to_group(tax_benefit_system) -> None: persons_ids: Iterable = [2, 0, 1, 4, 3] households_ids: Iterable = ["c", "a", "b"] @@ -555,7 +601,9 @@ def test_from_person_variable_to_group(tax_benefit_system): household_instance = simulation_builder.declare_entity("household", households_ids) simulation_builder.join_with_persons( - household_instance, persons_households, ["first_parent"] * 5 + household_instance, + persons_households, + ["first_parent"] * 5, ) simulation = simulation_builder.build(tax_benefit_system) @@ -567,14 +615,15 @@ def test_from_person_variable_to_group(tax_benefit_system): assert total_taxes / simulation.calculate("rent", period) == pytest.approx(1) -def test_simulation(tax_benefit_system): +def test_simulation(tax_benefit_system) -> None: input_yaml = """ salary: 2016-10: 12000 """ simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, test_runner.yaml.safe_load(input_yaml) + tax_benefit_system, + test_runner.yaml.safe_load(input_yaml), ) assert simulation.get_array("salary", "2016-10") == 12000 @@ -582,14 +631,15 @@ def test_simulation(tax_benefit_system): simulation.calculate("total_taxes", "2016-10") -def test_vectorial_input(tax_benefit_system): +def test_vectorial_input(tax_benefit_system) -> None: input_yaml = """ salary: 2016-10: [12000, 20000] """ simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, test_runner.yaml.safe_load(input_yaml) + tax_benefit_system, + test_runner.yaml.safe_load(input_yaml), ) tools.assert_near(simulation.get_array("salary", "2016-10"), [12000, 20000]) @@ -597,15 +647,16 @@ def test_vectorial_input(tax_benefit_system): simulation.calculate("total_taxes", "2016-10") -def test_fully_specified_entities(tax_benefit_system): +def test_fully_specified_entities(tax_benefit_system) -> None: simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, situation_examples.couple + tax_benefit_system, + situation_examples.couple, ) assert simulation.household.count == 1 assert simulation.persons.count == 2 -def test_single_entity_shortcut(tax_benefit_system): +def test_single_entity_shortcut(tax_benefit_system) -> None: input_yaml = """ persons: Alicia: {} @@ -615,12 +666,13 @@ def test_single_entity_shortcut(tax_benefit_system): """ simulation = SimulationBuilder().build_from_dict( - tax_benefit_system, test_runner.yaml.safe_load(input_yaml) + tax_benefit_system, + test_runner.yaml.safe_load(input_yaml), ) assert simulation.household.count == 1 -def test_order_preserved(tax_benefit_system): +def test_order_preserved(tax_benefit_system) -> None: input_yaml = """ persons: Javier: {} @@ -638,7 +690,7 @@ def test_order_preserved(tax_benefit_system): assert simulation.persons.ids == ["Javier", "Alicia", "Sarah", "Tom"] -def test_inconsistent_input(tax_benefit_system): +def test_inconsistent_input(tax_benefit_system) -> None: input_yaml = """ salary: 2016-10: [12000, 20000] @@ -647,6 +699,7 @@ def test_inconsistent_input(tax_benefit_system): """ with pytest.raises(ValueError) as error: SimulationBuilder().build_from_dict( - tax_benefit_system, test_runner.yaml.safe_load(input_yaml) + tax_benefit_system, + test_runner.yaml.safe_load(input_yaml), ) assert "its length is 3 while there are 2" in error.value.args[0] diff --git a/tests/core/test_simulations.py b/tests/core/test_simulations.py index 18050b6bc5..7f4897e776 100644 --- a/tests/core/test_simulations.py +++ b/tests/core/test_simulations.py @@ -6,7 +6,7 @@ from openfisca_core.simulations import SimulationBuilder -def test_calculate_full_tracer(tax_benefit_system): +def test_calculate_full_tracer(tax_benefit_system) -> None: simulation = SimulationBuilder().build_default_simulation(tax_benefit_system) simulation.trace = True simulation.calculate("income_tax", "2017-01") @@ -27,12 +27,12 @@ def test_calculate_full_tracer(tax_benefit_system): assert income_tax_node.parameters[0].value == 0.15 -def test_get_entity_not_found(tax_benefit_system): +def test_get_entity_not_found(tax_benefit_system) -> None: simulation = SimulationBuilder().build_default_simulation(tax_benefit_system) assert simulation.get_entity(plural="no_such_entities") is None -def test_clone(tax_benefit_system): +def test_clone(tax_benefit_system) -> None: simulation = SimulationBuilder().build_from_entities( tax_benefit_system, { @@ -59,7 +59,7 @@ def test_clone(tax_benefit_system): assert salary_holder_clone.population == simulation_clone.persons -def test_get_memory_usage(tax_benefit_system): +def test_get_memory_usage(tax_benefit_system) -> None: simulation = SimulationBuilder().build_from_entities(tax_benefit_system, single) simulation.calculate("disposable_income", "2017-01") memory_usage = simulation.get_memory_usage(variables=["salary"]) @@ -67,7 +67,7 @@ def test_get_memory_usage(tax_benefit_system): assert len(memory_usage["by_variable"]) == 1 -def test_invalidate_cache_when_spiral_error_detected(tax_benefit_system): +def test_invalidate_cache_when_spiral_error_detected(tax_benefit_system) -> None: simulation = SimulationBuilder().build_default_simulation(tax_benefit_system) tracer = simulation.tracer diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index 41acf68bc4..178b957ec4 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import csv import json import os @@ -21,34 +19,33 @@ from .parameters_fancy_indexing.test_fancy_indexing import parameters -class TestException(Exception): - ... +class TestException(Exception): ... class StubSimulation(Simulation): - def __init__(self): + def __init__(self) -> None: self.exception = None self.max_spiral_loops = 1 - def _calculate(self, variable, period): + def _calculate(self, variable, period) -> None: if self.exception: raise self.exception - def invalidate_cache_entry(self, variable, period): + def invalidate_cache_entry(self, variable, period) -> None: pass - def purge_cache_of_invalid_values(self): + def purge_cache_of_invalid_values(self) -> None: pass class MockTracer: - def record_calculation_start(self, variable, period): + def record_calculation_start(self, variable, period) -> None: self.calculation_start_recorded = True - def record_calculation_result(self, value): + def record_calculation_result(self, value) -> None: self.recorded_result = True - def record_calculation_end(self): + def record_calculation_end(self) -> None: self.calculation_end_recorded = True @@ -58,7 +55,7 @@ def tracer(): @mark.parametrize("tracer", [SimpleTracer(), FullTracer()]) -def test_stack_one_level(tracer): +def test_stack_one_level(tracer) -> None: tracer.record_calculation_start("a", 2017) assert len(tracer.stack) == 1 @@ -70,7 +67,7 @@ def test_stack_one_level(tracer): @mark.parametrize("tracer", [SimpleTracer(), FullTracer()]) -def test_stack_two_levels(tracer): +def test_stack_two_levels(tracer) -> None: tracer.record_calculation_start("a", 2017) tracer.record_calculation_start("b", 2017) @@ -87,7 +84,7 @@ def test_stack_two_levels(tracer): @mark.parametrize("tracer", [SimpleTracer(), FullTracer()]) -def test_tracer_contract(tracer): +def test_tracer_contract(tracer) -> None: simulation = StubSimulation() simulation.tracer = MockTracer() @@ -97,7 +94,7 @@ def test_tracer_contract(tracer): assert simulation.tracer.calculation_end_recorded -def test_exception_robustness(): +def test_exception_robustness() -> None: simulation = StubSimulation() simulation.tracer = MockTracer() simulation.exception = TestException(":-o") @@ -110,7 +107,7 @@ def test_exception_robustness(): @mark.parametrize("tracer", [SimpleTracer(), FullTracer()]) -def test_cycle_error(tracer): +def test_cycle_error(tracer) -> None: simulation = StubSimulation() simulation.tracer = tracer @@ -131,7 +128,7 @@ def test_cycle_error(tracer): @mark.parametrize("tracer", [SimpleTracer(), FullTracer()]) -def test_spiral_error(tracer): +def test_spiral_error(tracer) -> None: simulation = StubSimulation() simulation.tracer = tracer @@ -150,7 +147,7 @@ def test_spiral_error(tracer): ] -def test_full_tracer_one_calculation(tracer): +def test_full_tracer_one_calculation(tracer) -> None: tracer._enter_calculation("a", 2017) tracer._exit_calculation() @@ -161,7 +158,7 @@ def test_full_tracer_one_calculation(tracer): assert tracer.trees[0].children == [] -def test_full_tracer_2_branches(tracer): +def test_full_tracer_2_branches(tracer) -> None: tracer._enter_calculation("a", 2017) tracer._enter_calculation("b", 2017) tracer._exit_calculation() @@ -173,7 +170,7 @@ def test_full_tracer_2_branches(tracer): assert len(tracer.trees[0].children) == 2 -def test_full_tracer_2_trees(tracer): +def test_full_tracer_2_trees(tracer) -> None: tracer._enter_calculation("b", 2017) tracer._exit_calculation() tracer._enter_calculation("c", 2017) @@ -182,7 +179,7 @@ def test_full_tracer_2_trees(tracer): assert len(tracer.trees) == 2 -def test_full_tracer_3_generations(tracer): +def test_full_tracer_3_generations(tracer) -> None: tracer._enter_calculation("a", 2017) tracer._enter_calculation("b", 2017) tracer._enter_calculation("c", 2017) @@ -195,14 +192,14 @@ def test_full_tracer_3_generations(tracer): assert len(tracer.trees[0].children[0].children) == 1 -def test_full_tracer_variable_nb_requests(tracer): +def test_full_tracer_variable_nb_requests(tracer) -> None: tracer._enter_calculation("a", "2017-01") tracer._enter_calculation("a", "2017-02") assert tracer.get_nb_requests("a") == 2 -def test_simulation_calls_record_calculation_result(): +def test_simulation_calls_record_calculation_result() -> None: simulation = StubSimulation() simulation.tracer = MockTracer() @@ -211,7 +208,7 @@ def test_simulation_calls_record_calculation_result(): assert simulation.tracer.recorded_result -def test_record_calculation_result(tracer): +def test_record_calculation_result(tracer) -> None: tracer._enter_calculation("a", 2017) tracer.record_calculation_result(numpy.asarray(100)) tracer._exit_calculation() @@ -219,7 +216,7 @@ def test_record_calculation_result(tracer): assert tracer.trees[0].value == 100 -def test_flat_trace(tracer): +def test_flat_trace(tracer) -> None: tracer._enter_calculation("a", 2019) tracer._enter_calculation("b", 2019) tracer._exit_calculation() @@ -232,7 +229,7 @@ def test_flat_trace(tracer): assert trace["b<2019>"]["dependencies"] == [] -def test_flat_trace_serialize_vectorial_values(tracer): +def test_flat_trace_serialize_vectorial_values(tracer) -> None: tracer._enter_calculation("a", 2019) tracer.record_parameter_access("x.y.z", 2019, numpy.asarray([100, 200, 300])) tracer.record_calculation_result(numpy.asarray([10, 20, 30])) @@ -244,7 +241,7 @@ def test_flat_trace_serialize_vectorial_values(tracer): assert json.dumps(trace["a<2019>"]["parameters"]["x.y.z<2019>"]) -def test_flat_trace_with_parameter(tracer): +def test_flat_trace_with_parameter(tracer) -> None: tracer._enter_calculation("a", 2019) tracer.record_parameter_access("p", "2019-01-01", 100) tracer._exit_calculation() @@ -255,7 +252,7 @@ def test_flat_trace_with_parameter(tracer): assert trace["a<2019>"]["parameters"] == {"p<2019-01-01>": 100} -def test_flat_trace_with_cache(tracer): +def test_flat_trace_with_cache(tracer) -> None: tracer._enter_calculation("a", 2019) tracer._enter_calculation("b", 2019) tracer._enter_calculation("c", 2019) @@ -270,7 +267,7 @@ def test_flat_trace_with_cache(tracer): assert trace["b<2019>"]["dependencies"] == ["c<2019>"] -def test_calculation_time(): +def test_calculation_time() -> None: tracer = FullTracer() tracer._enter_calculation("a", 2019) @@ -322,7 +319,7 @@ def tracer_calc_time(): return tracer -def test_calculation_time_with_depth(tracer_calc_time): +def test_calculation_time_with_depth(tracer_calc_time) -> None: tracer = tracer_calc_time performance_json = tracer.performance_log._json() simulation_grand_children = performance_json["children"][0]["children"] @@ -331,7 +328,7 @@ def test_calculation_time_with_depth(tracer_calc_time): assert simulation_grand_children[0]["value"] == 700 -def test_flat_trace_calc_time(tracer_calc_time): +def test_flat_trace_calc_time(tracer_calc_time) -> None: tracer = tracer_calc_time flat_trace = tracer.get_flat_trace() @@ -343,11 +340,11 @@ def test_flat_trace_calc_time(tracer_calc_time): assert flat_trace["c<2019>"]["formula_time"] == 100 -def test_generate_performance_table(tracer_calc_time, tmpdir): +def test_generate_performance_table(tracer_calc_time, tmpdir) -> None: tracer = tracer_calc_time tracer.generate_performance_tables(tmpdir) - with open(os.path.join(tmpdir, "performance_table.csv"), "r") as csv_file: + with open(os.path.join(tmpdir, "performance_table.csv")) as csv_file: csv_reader = csv.DictReader(csv_file) csv_rows = list(csv_reader) @@ -358,9 +355,7 @@ def test_generate_performance_table(tracer_calc_time, tmpdir): assert float(a_row["calculation_time"]) == 1000 assert float(a_row["formula_time"]) == 190 - with open( - os.path.join(tmpdir, "aggregated_performance_table.csv"), "r" - ) as csv_file: + with open(os.path.join(tmpdir, "aggregated_performance_table.csv")) as csv_file: aggregated_csv_reader = csv.DictReader(csv_file) aggregated_csv_rows = list(aggregated_csv_reader) @@ -372,10 +367,10 @@ def test_generate_performance_table(tracer_calc_time, tmpdir): assert float(a_row["formula_time"]) == 190 + 200 -def test_get_aggregated_calculation_times(tracer_calc_time): +def test_get_aggregated_calculation_times(tracer_calc_time) -> None: perf_log = tracer_calc_time.performance_log aggregated_calculation_times = perf_log.aggregate_calculation_times( - tracer_calc_time.get_flat_trace() + tracer_calc_time.get_flat_trace(), ) assert aggregated_calculation_times["a"]["calculation_time"] == 1000 + 200 @@ -384,7 +379,7 @@ def test_get_aggregated_calculation_times(tracer_calc_time): assert aggregated_calculation_times["a"]["avg_formula_time"] == (190 + 200) / 2 -def test_rounding(): +def test_rounding() -> None: node_a = TraceNode("a", 2017) node_a.start = 1.23456789 node_a.end = node_a.start + 1.23456789e-03 @@ -401,7 +396,7 @@ def test_rounding(): ) # The rounding should not prevent from calculating a precise formula_time -def test_variable_stats(tracer): +def test_variable_stats(tracer) -> None: tracer._enter_calculation("A", 2017) tracer._enter_calculation("B", 2017) tracer._enter_calculation("B", 2017) @@ -412,7 +407,7 @@ def test_variable_stats(tracer): assert tracer.get_nb_requests("C") == 0 -def test_log_format(tracer): +def test_log_format(tracer) -> None: tracer._enter_calculation("A", 2017) tracer._enter_calculation("B", 2017) tracer.record_calculation_result(numpy.asarray([1])) @@ -425,7 +420,7 @@ def test_log_format(tracer): assert lines[1] == " B<2017> >> [1]" -def test_log_format_forest(tracer): +def test_log_format_forest(tracer) -> None: tracer._enter_calculation("A", 2017) tracer.record_calculation_result(numpy.asarray([1])) tracer._exit_calculation() @@ -438,7 +433,7 @@ def test_log_format_forest(tracer): assert lines[1] == " B<2017> >> [2]" -def test_log_aggregate(tracer): +def test_log_aggregate(tracer) -> None: tracer._enter_calculation("A", 2017) tracer.record_calculation_result(numpy.asarray([1])) tracer._exit_calculation() @@ -447,10 +442,10 @@ def test_log_aggregate(tracer): assert lines[0] == " A<2017> >> {'avg': 1.0, 'max': 1, 'min': 1}" -def test_log_aggregate_with_enum(tracer): +def test_log_aggregate_with_enum(tracer) -> None: tracer._enter_calculation("A", 2017) tracer.record_calculation_result( - HousingOccupancyStatus.encode(numpy.repeat("tenant", 100)) + HousingOccupancyStatus.encode(numpy.repeat("tenant", 100)), ) tracer._exit_calculation() lines = tracer.computation_log.lines(aggregate=True) @@ -461,7 +456,7 @@ def test_log_aggregate_with_enum(tracer): ) -def test_log_aggregate_with_strings(tracer): +def test_log_aggregate_with_strings(tracer) -> None: tracer._enter_calculation("A", 2017) tracer.record_calculation_result(numpy.repeat("foo", 100)) tracer._exit_calculation() @@ -470,7 +465,7 @@ def test_log_aggregate_with_strings(tracer): assert lines[0] == " A<2017> >> {'avg': '?', 'max': '?', 'min': '?'}" -def test_log_max_depth(tracer): +def test_log_max_depth(tracer) -> None: tracer._enter_calculation("A", 2017) tracer._enter_calculation("B", 2017) tracer._enter_calculation("C", 2017) @@ -489,10 +484,10 @@ def test_log_max_depth(tracer): assert len(tracer.computation_log.lines(max_depth=0)) == 0 -def test_no_wrapping(tracer): +def test_no_wrapping(tracer) -> None: tracer._enter_calculation("A", 2017) tracer.record_calculation_result( - HousingOccupancyStatus.encode(numpy.repeat("tenant", 100)) + HousingOccupancyStatus.encode(numpy.repeat("tenant", 100)), ) tracer._exit_calculation() lines = tracer.computation_log.lines() @@ -501,10 +496,10 @@ def test_no_wrapping(tracer): assert "\n" not in lines[0] -def test_trace_enums(tracer): +def test_trace_enums(tracer) -> None: tracer._enter_calculation("A", 2017) tracer.record_calculation_result( - HousingOccupancyStatus.encode(numpy.array(["tenant"])) + HousingOccupancyStatus.encode(numpy.array(["tenant"])), ) tracer._exit_calculation() lines = tracer.computation_log.lines() @@ -518,7 +513,7 @@ def test_trace_enums(tracer): family_status = numpy.asarray(["single", "couple", "single", "couple"]) -def check_tracing_params(accessor, param_key): +def check_tracing_params(accessor, param_key) -> None: tracer = FullTracer() tracer._enter_calculation("A", "2015-01") @@ -556,11 +551,11 @@ def check_tracing_params(accessor, param_key): ), # triple ], ) -def test_parameters(test): +def test_parameters(test) -> None: check_tracing_params(*test) -def test_browse_trace(): +def test_browse_trace() -> None: tracer = FullTracer() tracer._enter_calculation("B", 2017) diff --git a/tests/core/test_yaml.py b/tests/core/test_yaml.py index 4673665fcb..1672ea3453 100644 --- a/tests/core/test_yaml.py +++ b/tests/core/test_yaml.py @@ -19,82 +19,83 @@ def run_yaml_test(tax_benefit_system, path, options=None): if options is None: options = {} - result = run_tests(tax_benefit_system, yaml_path, options) - return result + return run_tests(tax_benefit_system, yaml_path, options) -def test_success(tax_benefit_system): +def test_success(tax_benefit_system) -> None: assert run_yaml_test(tax_benefit_system, "test_success.yml") == EXIT_OK -def test_fail(tax_benefit_system): +def test_fail(tax_benefit_system) -> None: assert run_yaml_test(tax_benefit_system, "test_failure.yaml") == EXIT_TESTSFAILED -def test_relative_error_margin_success(tax_benefit_system): +def test_relative_error_margin_success(tax_benefit_system) -> None: assert ( run_yaml_test(tax_benefit_system, "test_relative_error_margin.yaml") == EXIT_OK ) -def test_relative_error_margin_fail(tax_benefit_system): +def test_relative_error_margin_fail(tax_benefit_system) -> None: assert ( run_yaml_test(tax_benefit_system, "failing_test_relative_error_margin.yaml") == EXIT_TESTSFAILED ) -def test_absolute_error_margin_success(tax_benefit_system): +def test_absolute_error_margin_success(tax_benefit_system) -> None: assert ( run_yaml_test(tax_benefit_system, "test_absolute_error_margin.yaml") == EXIT_OK ) -def test_absolute_error_margin_fail(tax_benefit_system): +def test_absolute_error_margin_fail(tax_benefit_system) -> None: assert ( run_yaml_test(tax_benefit_system, "failing_test_absolute_error_margin.yaml") == EXIT_TESTSFAILED ) -def test_run_tests_from_directory(tax_benefit_system): +def test_run_tests_from_directory(tax_benefit_system) -> None: dir_path = os.path.join(yaml_tests_dir, "directory") assert run_yaml_test(tax_benefit_system, dir_path) == EXIT_OK -def test_with_reform(tax_benefit_system): +def test_with_reform(tax_benefit_system) -> None: assert run_yaml_test(tax_benefit_system, "test_with_reform.yaml") == EXIT_OK -def test_with_extension(tax_benefit_system): +def test_with_extension(tax_benefit_system) -> None: assert run_yaml_test(tax_benefit_system, "test_with_extension.yaml") == EXIT_OK -def test_with_anchors(tax_benefit_system): +def test_with_anchors(tax_benefit_system) -> None: assert run_yaml_test(tax_benefit_system, "test_with_anchors.yaml") == EXIT_OK -def test_run_tests_from_directory_fail(tax_benefit_system): +def test_run_tests_from_directory_fail(tax_benefit_system) -> None: assert run_yaml_test(tax_benefit_system, yaml_tests_dir) == EXIT_TESTSFAILED -def test_name_filter(tax_benefit_system): +def test_name_filter(tax_benefit_system) -> None: assert ( run_yaml_test( - tax_benefit_system, yaml_tests_dir, options={"name_filter": "success"} + tax_benefit_system, + yaml_tests_dir, + options={"name_filter": "success"}, ) == EXIT_OK ) -def test_shell_script(): +def test_shell_script() -> None: yaml_path = os.path.join(yaml_tests_dir, "test_success.yml") command = ["openfisca", "test", yaml_path, "-c", "openfisca_country_template"] with open(os.devnull, "wb") as devnull: subprocess.check_call(command, stdout=devnull, stderr=devnull) -def test_failing_shell_script(): +def test_failing_shell_script() -> None: yaml_path = os.path.join(yaml_tests_dir, "test_failure.yaml") command = ["openfisca", "test", yaml_path, "-c", "openfisca_dummy_country"] with open(os.devnull, "wb") as devnull: @@ -102,7 +103,7 @@ def test_failing_shell_script(): subprocess.check_call(command, stdout=devnull, stderr=devnull) -def test_shell_script_with_reform(): +def test_shell_script_with_reform() -> None: yaml_path = os.path.join(yaml_tests_dir, "test_with_reform_2.yaml") command = [ "openfisca", @@ -117,7 +118,7 @@ def test_shell_script_with_reform(): subprocess.check_call(command, stdout=devnull, stderr=devnull) -def test_shell_script_with_extension(): +def test_shell_script_with_extension() -> None: tests_dir = os.path.join(openfisca_extension_template.__path__[0], "tests") command = [ "openfisca", diff --git a/tests/core/tools/test_assert_near.py b/tests/core/tools/test_assert_near.py index 0d540a49e8..c351be0f9c 100644 --- a/tests/core/tools/test_assert_near.py +++ b/tests/core/tools/test_assert_near.py @@ -3,11 +3,11 @@ from openfisca_core.tools import assert_near -def test_date(): +def test_date() -> None: assert_near(numpy.array("2012-03-24", dtype="datetime64[D]"), "2012-03-24") -def test_enum(tax_benefit_system): +def test_enum(tax_benefit_system) -> None: possible_values = tax_benefit_system.variables[ "housing_occupancy_status" ].possible_values @@ -16,7 +16,7 @@ def test_enum(tax_benefit_system): assert_near(value, expected_value) -def test_enum_2(tax_benefit_system): +def test_enum_2(tax_benefit_system) -> None: possible_values = tax_benefit_system.variables[ "housing_occupancy_status" ].possible_values diff --git a/tests/core/tools/test_runner/test_yaml_runner.py b/tests/core/tools/test_runner/test_yaml_runner.py index bf04ade9bb..6a02d14cef 100644 --- a/tests/core/tools/test_runner/test_yaml_runner.py +++ b/tests/core/tools/test_runner/test_yaml_runner.py @@ -1,5 +1,3 @@ -from typing import List - import os import numpy @@ -14,7 +12,7 @@ class TaxBenefitSystem: - def __init__(self): + def __init__(self) -> None: self.variables = {"salary": TestVariable()} self.person_entity = Entity("person", "persons", None, "") self.person_entity.set_tax_benefit_system(self) @@ -25,7 +23,7 @@ def get_package_metadata(self): def apply_reform(self, path): return Reform(self) - def load_extension(self, extension): + def load_extension(self, extension) -> None: pass def entities_by_singular(self): @@ -45,27 +43,27 @@ def clone(self): class Reform(TaxBenefitSystem): - def __init__(self, baseline): + def __init__(self, baseline) -> None: self.baseline = baseline class Simulation: - def __init__(self): + def __init__(self) -> None: self.populations = {"person": None} - def get_population(self, plural=None): + def get_population(self, plural=None) -> None: return None class TestFile(YamlFile): - def __init__(self): + def __init__(self) -> None: self.config = None self.session = None self._nodeid = "testname" class TestItem(YamlItem): - def __init__(self, test): + def __init__(self, test) -> None: super().__init__("", TestFile(), TaxBenefitSystem(), test, {}) self.tax_benefit_system = self.baseline_tax_benefit_system @@ -76,7 +74,7 @@ class TestVariable(Variable): definition_period = DateUnit.ETERNITY value_type = float - def __init__(self): + def __init__(self) -> None: self.end = None self.entity = Entity("person", "persons", None, "") self.is_neutralized = False @@ -85,7 +83,7 @@ def __init__(self): @pytest.mark.skip(reason="Deprecated node constructor") -def test_variable_not_found(): +def test_variable_not_found() -> None: test = {"output": {"unknown_variable": 0}} with pytest.raises(errors.VariableNotFoundError) as excinfo: test_item = TestItem(test) @@ -93,7 +91,7 @@ def test_variable_not_found(): assert excinfo.value.variable_name == "unknown_variable" -def test_tax_benefit_systems_with_reform_cache(): +def test_tax_benefit_systems_with_reform_cache() -> None: baseline = TaxBenefitSystem() ab_tax_benefit_system = _get_tax_benefit_system(baseline, "ab", []) @@ -101,7 +99,7 @@ def test_tax_benefit_systems_with_reform_cache(): assert ab_tax_benefit_system != ba_tax_benefit_system -def test_reforms_formats(): +def test_reforms_formats() -> None: baseline = TaxBenefitSystem() lonely_reform_tbs = _get_tax_benefit_system(baseline, "lonely_reform", []) @@ -109,7 +107,7 @@ def test_reforms_formats(): assert lonely_reform_tbs == list_lonely_reform_tbs -def test_reforms_order(): +def test_reforms_order() -> None: baseline = TaxBenefitSystem() abba_tax_benefit_system = _get_tax_benefit_system(baseline, ["ab", "ba"], []) @@ -119,7 +117,7 @@ def test_reforms_order(): ) # keep reforms order in cache -def test_tax_benefit_systems_with_extensions_cache(): +def test_tax_benefit_systems_with_extensions_cache() -> None: baseline = TaxBenefitSystem() xy_tax_benefit_system = _get_tax_benefit_system(baseline, [], "xy") @@ -127,17 +125,19 @@ def test_tax_benefit_systems_with_extensions_cache(): assert xy_tax_benefit_system != yx_tax_benefit_system -def test_extensions_formats(): +def test_extensions_formats() -> None: baseline = TaxBenefitSystem() lonely_extension_tbs = _get_tax_benefit_system(baseline, [], "lonely_extension") list_lonely_extension_tbs = _get_tax_benefit_system( - baseline, [], ["lonely_extension"] + baseline, + [], + ["lonely_extension"], ) assert lonely_extension_tbs == list_lonely_extension_tbs -def test_extensions_order(): +def test_extensions_order() -> None: baseline = TaxBenefitSystem() xy_tax_benefit_system = _get_tax_benefit_system(baseline, [], ["x", "y"]) @@ -148,7 +148,7 @@ def test_extensions_order(): @pytest.mark.skip(reason="Deprecated node constructor") -def test_performance_graph_option_output(): +def test_performance_graph_option_output() -> None: test = { "input": {"salary": {"2017-01": 2000}}, "output": {"salary": {"2017-01": 2000}}, @@ -170,7 +170,7 @@ def test_performance_graph_option_output(): @pytest.mark.skip(reason="Deprecated node constructor") -def test_performance_tables_option_output(): +def test_performance_tables_option_output() -> None: test = { "input": {"salary": {"2017-01": 2000}}, "output": {"salary": {"2017-01": 2000}}, @@ -191,7 +191,7 @@ def test_performance_tables_option_output(): clean_performance_files(paths) -def clean_performance_files(paths: List[str]): +def clean_performance_files(paths: list[str]) -> None: for path in paths: if os.path.isfile(path): os.remove(path) diff --git a/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index 7bf85d9a46..58ea1372dd 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -1,4 +1,4 @@ -import numpy as np +import numpy from pytest import fixture from openfisca_country_template.entities import Person @@ -17,9 +17,9 @@ class monthly_variable(Variable): entity = Person definition_period = DateUnit.MONTH - def formula(person, period, parameters): + def formula(self, period, parameters): variable.calculation_count += 1 - return np.asarray([100]) + return numpy.asarray([100]) variable = monthly_variable() variable.calculation_count = calculation_count @@ -30,17 +30,16 @@ def formula(person, period, parameters): class PopulationMock: # Simulate a population for whom a variable has already been put in cache for January. - def __init__(self, variable): + def __init__(self, variable) -> None: self.variable = variable def __call__(self, variable_name: str, period): if period.start.month == 1: - return np.asarray([100]) - else: - return self.variable.get_formula(period)(self, period, None) + return numpy.asarray([100]) + return self.variable.get_formula(period)(self, period, None) -def test_without_annualize(monthly_variable): +def test_without_annualize(monthly_variable) -> None: period = periods.period(2019) person = PopulationMock(monthly_variable) @@ -54,7 +53,7 @@ def test_without_annualize(monthly_variable): assert yearly_sum == 1200 -def test_with_annualize(monthly_variable): +def test_with_annualize(monthly_variable) -> None: period = periods.period(2019) annualized_variable = get_annualized_variable(monthly_variable) @@ -69,10 +68,11 @@ def test_with_annualize(monthly_variable): assert yearly_sum == 100 * 12 -def test_with_partial_annualize(monthly_variable): +def test_with_partial_annualize(monthly_variable) -> None: period = periods.period("year:2018:2") annualized_variable = get_annualized_variable( - monthly_variable, periods.period(2018) + monthly_variable, + periods.period(2018), ) person = PopulationMock(annualized_variable) diff --git a/tests/core/variables/test_definition_period.py b/tests/core/variables/test_definition_period.py index 7938aaeaef..8ef9bfaa87 100644 --- a/tests/core/variables/test_definition_period.py +++ b/tests/core/variables/test_definition_period.py @@ -13,31 +13,31 @@ class TestVariable(Variable): return TestVariable -def test_weekday_variable(variable): +def test_weekday_variable(variable) -> None: variable.definition_period = periods.WEEKDAY assert variable() -def test_week_variable(variable): +def test_week_variable(variable) -> None: variable.definition_period = periods.WEEK assert variable() -def test_day_variable(variable): +def test_day_variable(variable) -> None: variable.definition_period = periods.DAY assert variable() -def test_month_variable(variable): +def test_month_variable(variable) -> None: variable.definition_period = periods.MONTH assert variable() -def test_year_variable(variable): +def test_year_variable(variable) -> None: variable.definition_period = periods.YEAR assert variable() -def test_eternity_variable(variable): +def test_eternity_variable(variable) -> None: variable.definition_period = periods.ETERNITY assert variable() diff --git a/tests/core/variables/test_variables.py b/tests/core/variables/test_variables.py index 3b2790bae7..d5d85a70d9 100644 --- a/tests/core/variables/test_variables.py +++ b/tests/core/variables/test_variables.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import datetime from pytest import fixture, mark, raises @@ -26,14 +24,16 @@ @fixture def couple(): return SimulationBuilder().build_from_entities( - tax_benefit_system, openfisca_country_template.situation_examples.couple + tax_benefit_system, + openfisca_country_template.situation_examples.couple, ) @fixture def simulation(): return SimulationBuilder().build_from_entities( - tax_benefit_system, openfisca_country_template.situation_examples.single + tax_benefit_system, + openfisca_country_template.situation_examples.single, ) @@ -41,16 +41,17 @@ def vectorize(individu, number): return individu.filled_array(number) -def check_error_at_add_variable(tax_benefit_system, variable, error_message_prefix): +def check_error_at_add_variable( + tax_benefit_system, variable, error_message_prefix +) -> None: try: tax_benefit_system.add_variable(variable) except ValueError as e: message = get_message(e) if not message or not message.startswith(error_message_prefix): + msg = f'Incorrect error message. Was expecting something starting by "{error_message_prefix}". Got: "{message}"' raise AssertionError( - 'Incorrect error message. Was expecting something starting by "{}". Got: "{}"'.format( - error_message_prefix, message - ) + msg, ) @@ -71,11 +72,11 @@ class variable__no_date(Variable): label = "Variable without date." -def test_before_add__variable__no_date(): +def test_before_add__variable__no_date() -> None: assert tax_benefit_system.variables.get("variable__no_date") is None -def test_variable__no_date(): +def test_variable__no_date() -> None: tax_benefit_system.add_variable(variable__no_date) variable = tax_benefit_system.variables["variable__no_date"] assert variable.end is None @@ -93,14 +94,14 @@ class variable__strange_end_attribute(Variable): end = "1989-00-00" -def test_variable__strange_end_attribute(): +def test_variable__strange_end_attribute() -> None: try: tax_benefit_system.add_variable(variable__strange_end_attribute) except ValueError as e: message = get_message(e) assert message.startswith( - "Incorrect 'end' attribute format in 'variable__strange_end_attribute'." + "Incorrect 'end' attribute format in 'variable__strange_end_attribute'.", ) # Check that Error at variable adding prevents it from registration in the taxbenefitsystem. @@ -121,12 +122,12 @@ class variable__end_attribute(Variable): tax_benefit_system.add_variable(variable__end_attribute) -def test_variable__end_attribute(): +def test_variable__end_attribute() -> None: variable = tax_benefit_system.variables["variable__end_attribute"] assert variable.end == datetime.date(1989, 12, 31) -def test_variable__end_attribute_set_input(simulation): +def test_variable__end_attribute_set_input(simulation) -> None: month_before_end = "1989-01" month_after_end = "1990-01" simulation.set_input("variable__end_attribute", month_before_end, 10) @@ -145,21 +146,21 @@ class end_attribute__one_simple_formula(Variable): label = "Variable with end attribute, one formula without date." end = "1989-12-31" - def formula(individu, period): - return vectorize(individu, 100) + def formula(self, period): + return vectorize(self, 100) tax_benefit_system.add_variable(end_attribute__one_simple_formula) -def test_formulas_attributes_single_formula(): +def test_formulas_attributes_single_formula() -> None: formulas = tax_benefit_system.variables[ "end_attribute__one_simple_formula" ].formulas assert formulas["0001-01-01"] is not None -def test_call__end_attribute__one_simple_formula(simulation): +def test_call__end_attribute__one_simple_formula(simulation) -> None: month = "1979-12" assert simulation.calculate("end_attribute__one_simple_formula", month) == 100 @@ -170,7 +171,7 @@ def test_call__end_attribute__one_simple_formula(simulation): assert simulation.calculate("end_attribute__one_simple_formula", month) == 0 -def test_dates__end_attribute__one_simple_formula(): +def test_dates__end_attribute__one_simple_formula() -> None: variable = tax_benefit_system.variables["end_attribute__one_simple_formula"] assert variable.end == datetime.date(1989, 12, 31) @@ -190,11 +191,11 @@ class no_end_attribute__one_formula__strange_name(Variable): definition_period = DateUnit.MONTH label = "Variable without end attribute, one stangely named formula." - def formula_2015_toto(individu, period): - return vectorize(individu, 100) + def formula_2015_toto(self, period): + return vectorize(self, 100) -def test_add__no_end_attribute__one_formula__strange_name(): +def test_add__no_end_attribute__one_formula__strange_name() -> None: check_error_at_add_variable( tax_benefit_system, no_end_attribute__one_formula__strange_name, @@ -211,14 +212,14 @@ class no_end_attribute__one_formula__start(Variable): definition_period = DateUnit.MONTH label = "Variable without end attribute, one dated formula." - def formula_2000_01_01(individu, period): - return vectorize(individu, 100) + def formula_2000_01_01(self, period): + return vectorize(self, 100) tax_benefit_system.add_variable(no_end_attribute__one_formula__start) -def test_call__no_end_attribute__one_formula__start(simulation): +def test_call__no_end_attribute__one_formula__start(simulation) -> None: month = "1999-12" assert simulation.calculate("no_end_attribute__one_formula__start", month) == 0 @@ -229,7 +230,7 @@ def test_call__no_end_attribute__one_formula__start(simulation): assert simulation.calculate("no_end_attribute__one_formula__start", month) == 100 -def test_dates__no_end_attribute__one_formula__start(): +def test_dates__no_end_attribute__one_formula__start() -> None: variable = tax_benefit_system.variables["no_end_attribute__one_formula__start"] assert variable.end is None @@ -245,15 +246,15 @@ class no_end_attribute__one_formula__eternity(Variable): ) # For this entity, this variable shouldn't evolve through time label = "Variable without end attribute, one dated formula." - def formula_2000_01_01(individu, period): - return vectorize(individu, 100) + def formula_2000_01_01(self, period): + return vectorize(self, 100) tax_benefit_system.add_variable(no_end_attribute__one_formula__eternity) @mark.xfail() -def test_call__no_end_attribute__one_formula__eternity(simulation): +def test_call__no_end_attribute__one_formula__eternity(simulation) -> None: month = "1999-12" assert simulation.calculate("no_end_attribute__one_formula__eternity", month) == 0 @@ -262,12 +263,12 @@ def test_call__no_end_attribute__one_formula__eternity(simulation): assert simulation.calculate("no_end_attribute__one_formula__eternity", month) == 100 -def test_call__no_end_attribute__one_formula__eternity_before(simulation): +def test_call__no_end_attribute__one_formula__eternity_before(simulation) -> None: month = "1999-12" assert simulation.calculate("no_end_attribute__one_formula__eternity", month) == 0 -def test_call__no_end_attribute__one_formula__eternity_after(simulation): +def test_call__no_end_attribute__one_formula__eternity_after(simulation) -> None: month = "2000-01" assert simulation.calculate("no_end_attribute__one_formula__eternity", month) == 100 @@ -281,17 +282,17 @@ class no_end_attribute__formulas__start_formats(Variable): definition_period = DateUnit.MONTH label = "Variable without end attribute, multiple dated formulas." - def formula_2000(individu, period): - return vectorize(individu, 100) + def formula_2000(self, period): + return vectorize(self, 100) - def formula_2010_01(individu, period): - return vectorize(individu, 200) + def formula_2010_01(self, period): + return vectorize(self, 200) tax_benefit_system.add_variable(no_end_attribute__formulas__start_formats) -def test_formulas_attributes_dated_formulas(): +def test_formulas_attributes_dated_formulas() -> None: formulas = tax_benefit_system.variables[ "no_end_attribute__formulas__start_formats" ].formulas @@ -300,7 +301,7 @@ def test_formulas_attributes_dated_formulas(): assert formulas["2010-01-01"] is not None -def test_get_formulas(): +def test_get_formulas() -> None: variable = tax_benefit_system.variables["no_end_attribute__formulas__start_formats"] formula_2000 = variable.formulas["2000-01-01"] formula_2010 = variable.formulas["2010-01-01"] @@ -313,7 +314,7 @@ def test_get_formulas(): assert variable.get_formula("2010-01-01") == formula_2010 -def test_call__no_end_attribute__formulas__start_formats(simulation): +def test_call__no_end_attribute__formulas__start_formats(simulation) -> None: month = "1999-12" assert simulation.calculate("no_end_attribute__formulas__start_formats", month) == 0 @@ -342,14 +343,14 @@ class no_attribute__formulas__different_names__dates_overlap(Variable): definition_period = DateUnit.MONTH label = "Variable, no end attribute, multiple dated formulas with different names but same dates." - def formula_2000(individu, period): - return vectorize(individu, 100) + def formula_2000(self, period): + return vectorize(self, 100) - def formula_2000_01_01(individu, period): - return vectorize(individu, 200) + def formula_2000_01_01(self, period): + return vectorize(self, 200) -def test_add__no_attribute__formulas__different_names__dates_overlap(): +def test_add__no_attribute__formulas__different_names__dates_overlap() -> None: # Variable isn't registered in the taxbenefitsystem check_error_at_add_variable( tax_benefit_system, @@ -367,21 +368,22 @@ class no_attribute__formulas__different_names__no_overlap(Variable): definition_period = DateUnit.MONTH label = "Variable, no end attribute, multiple dated formulas with different names and no date overlap." - def formula_2000_01_01(individu, period): - return vectorize(individu, 100) + def formula_2000_01_01(self, period): + return vectorize(self, 100) - def formula_2010_01_01(individu, period): - return vectorize(individu, 200) + def formula_2010_01_01(self, period): + return vectorize(self, 200) tax_benefit_system.add_variable(no_attribute__formulas__different_names__no_overlap) -def test_call__no_attribute__formulas__different_names__no_overlap(simulation): +def test_call__no_attribute__formulas__different_names__no_overlap(simulation) -> None: month = "2009-12" assert ( simulation.calculate( - "no_attribute__formulas__different_names__no_overlap", month + "no_attribute__formulas__different_names__no_overlap", + month, ) == 100 ) @@ -389,7 +391,8 @@ def test_call__no_attribute__formulas__different_names__no_overlap(simulation): month = "2015-05" assert ( simulation.calculate( - "no_attribute__formulas__different_names__no_overlap", month + "no_attribute__formulas__different_names__no_overlap", + month, ) == 200 ) @@ -408,14 +411,14 @@ class end_attribute__one_formula__start(Variable): label = "Variable with end attribute, one dated formula." end = "2001-12-31" - def formula_2000_01_01(individu, period): - return vectorize(individu, 100) + def formula_2000_01_01(self, period): + return vectorize(self, 100) tax_benefit_system.add_variable(end_attribute__one_formula__start) -def test_call__end_attribute__one_formula__start(simulation): +def test_call__end_attribute__one_formula__start(simulation) -> None: month = "1980-01" assert simulation.calculate("end_attribute__one_formula__start", month) == 0 @@ -436,11 +439,11 @@ class stop_attribute_before__one_formula__start(Variable): label = "Variable with stop attribute only coming before formula start." end = "1990-01-01" - def formula_2000_01_01(individu, period): - return vectorize(individu, 0) + def formula_2000_01_01(self, period): + return vectorize(self, 0) -def test_add__stop_attribute_before__one_formula__start(): +def test_add__stop_attribute_before__one_formula__start() -> None: check_error_at_add_variable( tax_benefit_system, stop_attribute_before__one_formula__start, @@ -460,14 +463,14 @@ class end_attribute_restrictive__one_formula(Variable): ) end = "2001-01-01" - def formula_2001_01_01(individu, period): - return vectorize(individu, 100) + def formula_2001_01_01(self, period): + return vectorize(self, 100) tax_benefit_system.add_variable(end_attribute_restrictive__one_formula) -def test_call__end_attribute_restrictive__one_formula(simulation): +def test_call__end_attribute_restrictive__one_formula(simulation) -> None: month = "2000-12" assert simulation.calculate("end_attribute_restrictive__one_formula", month) == 0 @@ -488,20 +491,20 @@ class end_attribute__formulas__different_names(Variable): label = "Variable with end attribute, multiple dated formulas with different names." end = "2010-12-31" - def formula_2000_01_01(individu, period): - return vectorize(individu, 100) + def formula_2000_01_01(self, period): + return vectorize(self, 100) - def formula_2005_01_01(individu, period): - return vectorize(individu, 200) + def formula_2005_01_01(self, period): + return vectorize(self, 200) - def formula_2010_01_01(individu, period): - return vectorize(individu, 300) + def formula_2010_01_01(self, period): + return vectorize(self, 300) tax_benefit_system.add_variable(end_attribute__formulas__different_names) -def test_call__end_attribute__formulas__different_names(simulation): +def test_call__end_attribute__formulas__different_names(simulation) -> None: month = "2000-01" assert ( simulation.calculate("end_attribute__formulas__different_names", month) == 100 @@ -518,20 +521,22 @@ def test_call__end_attribute__formulas__different_names(simulation): ) -def test_get_formula(simulation): +def test_get_formula(simulation) -> None: person = simulation.person disposable_income_formula = tax_benefit_system.get_variable( - "disposable_income" + "disposable_income", ).get_formula() disposable_income = person("disposable_income", "2017-01") disposable_income_2 = disposable_income_formula( - person, "2017-01", None + person, + "2017-01", + None, ) # No need for parameters here assert_near(disposable_income, disposable_income_2) -def test_unexpected_attr(): +def test_unexpected_attr() -> None: class variable_with_strange_attr(Variable): value_type = int entity = Person diff --git a/tests/fixtures/appclient.py b/tests/fixtures/appclient.py index 5edcfc2c98..692747d393 100644 --- a/tests/fixtures/appclient.py +++ b/tests/fixtures/appclient.py @@ -15,8 +15,10 @@ def test_client(tax_benefit_system): from openfisca_country_template import entities from openfisca_core import periods from openfisca_core.variables import Variable + ... + class new_variable(Variable): value_type = float entity = entities.Person @@ -24,11 +26,11 @@ class new_variable(Variable): label = "New variable" reference = "https://law.gov.example/new_variable" # Always use the most official source + tax_benefit_system.add_variable(new_variable) flask_app = app.create_app(tax_benefit_system) """ - # Create the test API client flask_app = app.create_app(tax_benefit_system) return flask_app.test_client() diff --git a/tests/fixtures/entities.py b/tests/fixtures/entities.py index 4d103f10d3..6670a68da1 100644 --- a/tests/fixtures/entities.py +++ b/tests/fixtures/entities.py @@ -7,25 +7,29 @@ class TestEntity(Entity): def get_variable( - self, variable_name: str, check_existence: bool = False + self, + variable_name: str, + check_existence: bool = False, ) -> TestVariable: result = TestVariable(self) result.name = variable_name return result - def check_variable_defined_for_entity(self, variable_name: str): + def check_variable_defined_for_entity(self, variable_name: str) -> bool: return True class TestGroupEntity(GroupEntity): def get_variable( - self, variable_name: str, check_existence: bool = False + self, + variable_name: str, + check_existence: bool = False, ) -> TestVariable: result = TestVariable(self) result.name = variable_name return result - def check_variable_defined_for_entity(self, variable_name: str): + def check_variable_defined_for_entity(self, variable_name: str) -> bool: return True diff --git a/tests/fixtures/extensions.py b/tests/fixtures/extensions.py index 631dcbc0d7..bc4e85fe72 100644 --- a/tests/fixtures/extensions.py +++ b/tests/fixtures/extensions.py @@ -3,16 +3,16 @@ import pytest -@pytest.fixture() -def test_country_package_name(): +@pytest.fixture +def test_country_package_name() -> str: return "openfisca_country_template" -@pytest.fixture() -def test_extension_package_name(): +@pytest.fixture +def test_extension_package_name() -> str: return "openfisca_extension_template" -@pytest.fixture() +@pytest.fixture def distribution(test_country_package_name): return metadata.distribution(test_country_package_name) diff --git a/tests/fixtures/simulations.py b/tests/fixtures/simulations.py index 6df19ba27f..53120b60d9 100644 --- a/tests/fixtures/simulations.py +++ b/tests/fixtures/simulations.py @@ -24,6 +24,4 @@ def make_simulation(): def _simulation(simulation_builder, tax_benefit_system, variables, period): simulation_builder.set_default_period(period) - simulation = simulation_builder.build_from_variables(tax_benefit_system, variables) - - return simulation + return simulation_builder.build_from_variables(tax_benefit_system, variables) diff --git a/tests/fixtures/variables.py b/tests/fixtures/variables.py index aab7cda58d..2deccf5891 100644 --- a/tests/fixtures/variables.py +++ b/tests/fixtures/variables.py @@ -6,6 +6,6 @@ class TestVariable(Variable): definition_period = DateUnit.ETERNITY value_type = float - def __init__(self, entity): + def __init__(self, entity) -> None: self.__class__.entity = entity super().__init__() diff --git a/tests/web_api/case_with_extension/test_extensions.py b/tests/web_api/case_with_extension/test_extensions.py index be5ee6bf24..2c688232f8 100644 --- a/tests/web_api/case_with_extension/test_extensions.py +++ b/tests/web_api/case_with_extension/test_extensions.py @@ -6,7 +6,7 @@ from openfisca_web_api.app import create_app -@pytest.fixture() +@pytest.fixture def tax_benefit_system(test_country_package_name, test_extension_package_name): return build_tax_benefit_system( test_country_package_name, @@ -15,25 +15,25 @@ def tax_benefit_system(test_country_package_name, test_extension_package_name): ) -@pytest.fixture() +@pytest.fixture def extended_subject(tax_benefit_system): return create_app(tax_benefit_system).test_client() -def test_return_code(extended_subject): +def test_return_code(extended_subject) -> None: parameters_response = extended_subject.get("/parameters") assert parameters_response.status_code == client.OK -def test_return_code_existing_parameter(extended_subject): +def test_return_code_existing_parameter(extended_subject) -> None: extension_parameter_response = extended_subject.get( - "/parameter/local_town.child_allowance.amount" + "/parameter/local_town.child_allowance.amount", ) assert extension_parameter_response.status_code == client.OK -def test_return_code_existing_variable(extended_subject): +def test_return_code_existing_variable(extended_subject) -> None: extension_variable_response = extended_subject.get( - "/variable/local_town_child_allowance" + "/variable/local_town_child_allowance", ) assert extension_variable_response.status_code == client.OK diff --git a/tests/web_api/case_with_reform/test_reforms.py b/tests/web_api/case_with_reform/test_reforms.py index afcb811443..f0895cf189 100644 --- a/tests/web_api/case_with_reform/test_reforms.py +++ b/tests/web_api/case_with_reform/test_reforms.py @@ -6,7 +6,7 @@ from openfisca_web_api import app -@pytest.fixture() +@pytest.fixture def test_reforms_path(test_country_package_name): return [ f"{test_country_package_name}.reforms.add_dynamic_variable.add_dynamic_variable", @@ -29,37 +29,37 @@ def client(test_country_package_name, test_reforms_path): return app.create_app(tax_benefit_system).test_client() -def test_return_code_of_dynamic_variable(client): +def test_return_code_of_dynamic_variable(client) -> None: result = client.get("/variable/goes_to_school") assert result.status_code == http.client.OK -def test_return_code_of_has_car_variable(client): +def test_return_code_of_has_car_variable(client) -> None: result = client.get("/variable/has_car") assert result.status_code == http.client.OK -def test_return_code_of_new_tax_variable(client): +def test_return_code_of_new_tax_variable(client) -> None: result = client.get("/variable/new_tax") assert result.status_code == http.client.OK -def test_return_code_of_social_security_contribution_variable(client): +def test_return_code_of_social_security_contribution_variable(client) -> None: result = client.get("/variable/social_security_contribution") assert result.status_code == http.client.OK -def test_return_code_of_social_security_contribution_parameter(client): +def test_return_code_of_social_security_contribution_parameter(client) -> None: result = client.get("/parameter/taxes.social_security_contribution") assert result.status_code == http.client.OK -def test_return_code_of_basic_income_variable(client): +def test_return_code_of_basic_income_variable(client) -> None: result = client.get("/variable/basic_income") assert result.status_code == http.client.OK diff --git a/tests/web_api/loader/test_parameters.py b/tests/web_api/loader/test_parameters.py index 2b6be58916..f44632ce49 100644 --- a/tests/web_api/loader/test_parameters.py +++ b/tests/web_api/loader/test_parameters.py @@ -1,46 +1,44 @@ -# -*- coding: utf-8 -*- - from openfisca_core.parameters import Scale from openfisca_web_api.loader.parameters import build_api_parameter, build_api_scale -def test_build_rate_scale(): - """Extracts a 'rate' children from a bracket collection""" +def test_build_rate_scale() -> None: + """Extracts a 'rate' children from a bracket collection.""" data = { "brackets": [ { "rate": {"2014-01-01": {"value": 0.5}}, "threshold": {"2014-01-01": {"value": 1}}, - } - ] + }, + ], } rate = Scale("this rate", data, None) assert build_api_scale(rate, "rate") == {"2014-01-01": {1: 0.5}} -def test_build_amount_scale(): - """Extracts an 'amount' children from a bracket collection""" +def test_build_amount_scale() -> None: + """Extracts an 'amount' children from a bracket collection.""" data = { "brackets": [ { "amount": {"2014-01-01": {"value": 0}}, "threshold": {"2014-01-01": {"value": 1}}, - } - ] + }, + ], } rate = Scale("that amount", data, None) assert build_api_scale(rate, "amount") == {"2014-01-01": {1: 0}} -def test_full_rate_scale(): - """Serializes a 'rate' scale parameter""" +def test_full_rate_scale() -> None: + """Serializes a 'rate' scale parameter.""" data = { "brackets": [ { "rate": {"2014-01-01": {"value": 0.5}}, "threshold": {"2014-01-01": {"value": 1}}, - } - ] + }, + ], } scale = Scale("rate", data, None) api_scale = build_api_parameter(scale, {}) @@ -52,15 +50,15 @@ def test_full_rate_scale(): } -def test_walk_node_amount_scale(): - """Serializes an 'amount' scale parameter""" +def test_walk_node_amount_scale() -> None: + """Serializes an 'amount' scale parameter.""" data = { "brackets": [ { "amount": {"2014-01-01": {"value": 0}}, "threshold": {"2014-01-01": {"value": 1}}, - } - ] + }, + ], } scale = Scale("amount", data, None) api_scale = build_api_parameter(scale, {}) diff --git a/tests/web_api/test_calculate.py b/tests/web_api/test_calculate.py index d5d64c3c38..4d69dae9ab 100644 --- a/tests/web_api/test_calculate.py +++ b/tests/web_api/test_calculate.py @@ -12,14 +12,18 @@ def post_json(client, data=None, file=None): if file: file_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "assets", file + os.path.dirname(os.path.abspath(__file__)), + "assets", + file, ) - with open(file_path, "r") as file: + with open(file_path) as file: data = file.read() return client.post("/calculate", data=data, content_type="application/json") -def check_response(client, data, expected_error_code, path_to_check, content_to_check): +def check_response( + client, data, expected_error_code, path_to_check, content_to_check +) -> None: response = post_json(client, data) assert response.status_code == expected_error_code json_response = json.loads(response.data.decode("utf-8")) @@ -138,11 +142,11 @@ def check_response(client, data, expected_error_code, path_to_check, content_to_ ), ], ) -def test_responses(test_client, test): +def test_responses(test_client, test) -> None: check_response(test_client, *test) -def test_basic_calculation(test_client): +def test_basic_calculation(test_client) -> None: simulation_json = json.dumps( { "persons": { @@ -166,7 +170,7 @@ def test_basic_calculation(test_client): "accommodation_size": {"2017-01": 300}, }, }, - } + }, ) response = post_json(test_client, simulation_json) @@ -184,7 +188,8 @@ def test_basic_calculation(test_client): assert dpath.util.get(response_json, "persons/bob/basic_income/2017-12") == 600 assert ( dpath.util.get( - response_json, "persons/bob/social_security_contribution/2017-12" + response_json, + "persons/bob/social_security_contribution/2017-12", ) == 816 ) # From social_security_contribution.yaml test @@ -194,7 +199,7 @@ def test_basic_calculation(test_client): ) -def test_enums_sending_identifier(test_client): +def test_enums_sending_identifier(test_client) -> None: simulation_json = json.dumps( { "persons": {"bill": {}}, @@ -204,9 +209,9 @@ def test_enums_sending_identifier(test_client): "housing_tax": {"2017": None}, "accommodation_size": {"2017-01": 300}, "housing_occupancy_status": {"2017-01": "free_lodger"}, - } + }, }, - } + }, ) response = post_json(test_client, simulation_json) @@ -215,7 +220,7 @@ def test_enums_sending_identifier(test_client): assert dpath.util.get(response_json, "households/_/housing_tax/2017") == 0 -def test_enum_output(test_client): +def test_enum_output(test_client) -> None: simulation_json = json.dumps( { "persons": { @@ -227,7 +232,7 @@ def test_enum_output(test_client): "housing_occupancy_status": {"2017-01": None}, }, }, - } + }, ) response = post_json(test_client, simulation_json) @@ -239,7 +244,7 @@ def test_enum_output(test_client): ) -def test_enum_wrong_value(test_client): +def test_enum_wrong_value(test_client) -> None: simulation_json = json.dumps( { "persons": { @@ -251,7 +256,7 @@ def test_enum_wrong_value(test_client): "housing_occupancy_status": {"2017-01": "Unknown value lodger"}, }, }, - } + }, ) response = post_json(test_client, simulation_json) @@ -259,26 +264,27 @@ def test_enum_wrong_value(test_client): response_json = json.loads(response.data.decode("utf-8")) message = "Possible values are ['owner', 'tenant', 'free_lodger', 'homeless']" text = dpath.util.get( - response_json, "households/_/housing_occupancy_status/2017-01" + response_json, + "households/_/housing_occupancy_status/2017-01", ) assert message in text -def test_encoding_variable_value(test_client): +def test_encoding_variable_value(test_client) -> None: simulation_json = json.dumps( { "persons": {"toto": {}}, "households": { "_": { "housing_occupancy_status": { - "2017-07": "Locataire ou sous-locataire d‘un logement loué vide non-HLM" + "2017-07": "Locataire ou sous-locataire d‘un logement loué vide non-HLM", }, "parent": [ "toto", ], - } + }, }, - } + }, ) # No UnicodeDecodeError @@ -287,17 +293,18 @@ def test_encoding_variable_value(test_client): response_json = json.loads(response.data.decode("utf-8")) message = "'Locataire ou sous-locataire d‘un logement loué vide non-HLM' is not a known value for 'housing_occupancy_status'. Possible values are " text = dpath.util.get( - response_json, "households/_/housing_occupancy_status/2017-07" + response_json, + "households/_/housing_occupancy_status/2017-07", ) assert message in text -def test_encoding_entity_name(test_client): +def test_encoding_entity_name(test_client) -> None: simulation_json = json.dumps( { "persons": {"O‘Ryan": {}, "Renée": {}}, "households": {"_": {"parents": ["O‘Ryan", "Renée"]}}, - } + }, ) # No UnicodeDecodeError @@ -311,7 +318,7 @@ def test_encoding_entity_name(test_client): assert message in text -def test_encoding_period_id(test_client): +def test_encoding_period_id(test_client) -> None: simulation_json = json.dumps( { "persons": { @@ -324,9 +331,9 @@ def test_encoding_period_id(test_client): "housing_tax": {"à": 400}, "accommodation_size": {"2017-01": 300}, "housing_occupancy_status": {"2017-01": "tenant"}, - } + }, }, - } + }, ) # No UnicodeDecodeError @@ -341,19 +348,21 @@ def test_encoding_period_id(test_client): assert message in text -def test_str_variable(test_client): +def test_str_variable(test_client) -> None: new_couple = copy.deepcopy(couple) new_couple["households"]["_"]["postal_code"] = {"2017-01": None} simulation_json = json.dumps(new_couple) response = test_client.post( - "/calculate", data=simulation_json, content_type="application/json" + "/calculate", + data=simulation_json, + content_type="application/json", ) assert response.status_code == client.OK -def test_periods(test_client): +def test_periods(test_client) -> None: simulation_json = json.dumps( { "persons": {"bill": {}}, @@ -362,9 +371,9 @@ def test_periods(test_client): "parents": ["bill"], "housing_tax": {"2017": None}, "housing_occupancy_status": {"2017-01": None}, - } + }, }, - } + }, ) response = post_json(test_client, simulation_json) @@ -373,19 +382,20 @@ def test_periods(test_client): response_json = json.loads(response.data.decode("utf-8")) yearly_variable = dpath.util.get( - response_json, "households/_/housing_tax" + response_json, + "households/_/housing_tax", ) # web api year is an int assert yearly_variable == {"2017": 200.0} monthly_variable = dpath.util.get( - response_json, "households/_/housing_occupancy_status" + response_json, + "households/_/housing_occupancy_status", ) # web api month is a string assert monthly_variable == {"2017-01": "tenant"} -def test_two_periods(test_client): - """ - Test `calculate` on a request with mixed types periods: yearly periods following +def test_two_periods(test_client) -> None: + """Test `calculate` on a request with mixed types periods: yearly periods following monthly or daily periods to check dpath limitation on numeric keys (yearly periods). Made to test the case where we have more than one path with a numeric in it. See https://github.com/dpath-maintainers/dpath-python/issues/160 for more informations. @@ -398,9 +408,9 @@ def test_two_periods(test_client): "parents": ["bill"], "housing_tax": {"2017": None, "2018": None}, "housing_occupancy_status": {"2017-01": None, "2018-01": None}, - } + }, }, - } + }, ) response = post_json(test_client, simulation_json) @@ -409,17 +419,19 @@ def test_two_periods(test_client): response_json = json.loads(response.data.decode("utf-8")) yearly_variable = dpath.util.get( - response_json, "households/_/housing_tax" + response_json, + "households/_/housing_tax", ) # web api year is an int assert yearly_variable == {"2017": 200.0, "2018": 200.0} monthly_variable = dpath.util.get( - response_json, "households/_/housing_occupancy_status" + response_json, + "households/_/housing_occupancy_status", ) # web api month is a string assert monthly_variable == {"2017-01": "tenant", "2018-01": "tenant"} -def test_handle_period_mismatch_error(test_client): +def test_handle_period_mismatch_error(test_client) -> None: variable = "housing_tax" period = "2017-01" @@ -430,9 +442,9 @@ def test_handle_period_mismatch_error(test_client): "_": { "parents": ["bill"], variable: {period: 400}, - } + }, }, - } + }, ) response = post_json(test_client, simulation_json) @@ -445,9 +457,8 @@ def test_handle_period_mismatch_error(test_client): assert message in error -def test_gracefully_handle_unexpected_errors(test_client): - """ - Context +def test_gracefully_handle_unexpected_errors(test_client) -> None: + """Context. ======= Whenever an exception is raised by the calculation engine, the API will try @@ -466,7 +477,7 @@ def test_gracefully_handle_unexpected_errors(test_client): In the `country-template`, Housing Tax is only defined from 2010 onwards. The calculation engine should therefore raise an exception `ParameterNotFound`. The API is not expecting this, but she should handle the situation nonetheless. - """ # noqa RST399 + """ variable = "housing_tax" period = "1234-05-06" @@ -481,9 +492,9 @@ def test_gracefully_handle_unexpected_errors(test_client): variable: { period: None, }, - } + }, }, - } + }, ) response = post_json(test_client, simulation_json) diff --git a/tests/web_api/test_entities.py b/tests/web_api/test_entities.py index afb909ef57..e7d0ef5b9b 100644 --- a/tests/web_api/test_entities.py +++ b/tests/web_api/test_entities.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import json from http import client @@ -8,12 +6,12 @@ # /entities -def test_return_code(test_client): +def test_return_code(test_client) -> None: entities_response = test_client.get("/entities") assert entities_response.status_code == client.OK -def test_response_data(test_client): +def test_response_data(test_client) -> None: entities_response = test_client.get("/entities") entities_dict = json.loads(entities_response.data.decode("utf-8")) test_documentation = entities.Household.doc.strip() diff --git a/tests/web_api/test_headers.py b/tests/web_api/test_headers.py index c5464d91b1..dc95437a09 100644 --- a/tests/web_api/test_headers.py +++ b/tests/web_api/test_headers.py @@ -1,10 +1,10 @@ -def test_package_name_header(test_client, distribution): +def test_package_name_header(test_client, distribution) -> None: name = distribution.metadata.get("Name").lower() parameters_response = test_client.get("/parameters") assert parameters_response.headers.get("Country-Package") == name -def test_package_version_header(test_client, distribution): +def test_package_version_header(test_client, distribution) -> None: version = distribution.metadata.get("Version") parameters_response = test_client.get("/parameters") assert parameters_response.headers.get("Country-Package-Version") == version diff --git a/tests/web_api/test_helpers.py b/tests/web_api/test_helpers.py index 5b22a57b47..a1725cdfbf 100644 --- a/tests/web_api/test_helpers.py +++ b/tests/web_api/test_helpers.py @@ -6,7 +6,7 @@ dir_path = os.path.join(os.path.dirname(__file__), "assets") -def test_build_api_values_history(): +def test_build_api_values_history() -> None: file_path = os.path.join(dir_path, "test_helpers.yaml") parameter = load_parameter_file(name="dummy_name", file_path=file_path) @@ -18,7 +18,7 @@ def test_build_api_values_history(): assert parameters.build_api_values_history(parameter) == values -def test_build_api_values_history_with_stop_date(): +def test_build_api_values_history_with_stop_date() -> None: file_path = os.path.join(dir_path, "test_helpers_with_stop_date.yaml") parameter = load_parameter_file(name="dummy_name", file_path=file_path) @@ -32,7 +32,7 @@ def test_build_api_values_history_with_stop_date(): assert parameters.build_api_values_history(parameter) == values -def test_get_value(): +def test_get_value() -> None: values = {"2013-01-01": 0.03, "2017-01-01": 0.02, "2015-01-01": 0.04} assert parameters.get_value("2013-01-01", values) == 0.03 @@ -43,7 +43,7 @@ def test_get_value(): assert parameters.get_value("2018-01-01", values) == 0.02 -def test_get_value_with_none(): +def test_get_value_with_none() -> None: values = {"2015-01-01": 0.04, "2017-01-01": None} assert parameters.get_value("2016-12-31", values) == 0.04 diff --git a/tests/web_api/test_parameters.py b/tests/web_api/test_parameters.py index 762193fc2d..77fee8f7ea 100644 --- a/tests/web_api/test_parameters.py +++ b/tests/web_api/test_parameters.py @@ -10,12 +10,12 @@ GITHUB_URL_REGEX = r"^https://github\.com/openfisca/country-template/blob/\d+\.\d+\.\d+((.dev|rc)\d+)?/openfisca_country_template/parameters/(.)+\.yaml$" -def test_return_code(test_client): +def test_return_code(test_client) -> None: parameters_response = test_client.get("/parameters") assert parameters_response.status_code == client.OK -def test_response_data(test_client): +def test_response_data(test_client) -> None: parameters_response = test_client.get("/parameters") parameters = json.loads(parameters_response.data.decode("utf-8")) @@ -29,25 +29,25 @@ def test_response_data(test_client): # /parameter/ -def test_error_code_non_existing_parameter(test_client): +def test_error_code_non_existing_parameter(test_client) -> None: response = test_client.get("/parameter/non/existing.parameter") assert response.status_code == client.NOT_FOUND -def test_return_code_existing_parameter(test_client): +def test_return_code_existing_parameter(test_client) -> None: response = test_client.get("/parameter/taxes/income_tax_rate") assert response.status_code == client.OK -def test_legacy_parameter_route(test_client): +def test_legacy_parameter_route(test_client) -> None: response = test_client.get("/parameter/taxes.income_tax_rate") assert response.status_code == client.OK -def test_parameter_values(test_client): +def test_parameter_values(test_client) -> None: response = test_client.get("/parameter/taxes/income_tax_rate") parameter = json.loads(response.data) - assert sorted(list(parameter.keys())), [ + assert sorted(parameter.keys()), [ "description", "id", "metadata", @@ -69,7 +69,7 @@ def test_parameter_values(test_client): # 'documentation' attribute exists only when a value is defined response = test_client.get("/parameter/benefits/housing_allowance") parameter = json.loads(response.data) - assert sorted(list(parameter.keys())), [ + assert sorted(parameter.keys()), [ "description", "documentation", "id", @@ -82,11 +82,11 @@ def test_parameter_values(test_client): ) -def test_parameter_node(tax_benefit_system, test_client): +def test_parameter_node(tax_benefit_system, test_client) -> None: response = test_client.get("/parameter/benefits") assert response.status_code == client.OK parameter = json.loads(response.data) - assert sorted(list(parameter.keys())), [ + assert sorted(parameter.keys()), [ "description", "documentation", "id", @@ -107,20 +107,22 @@ def test_parameter_node(tax_benefit_system, test_client): assert "description" in parameter["subparams"]["basic_income"] assert parameter["subparams"]["basic_income"]["description"] == getattr( - model_benefits.basic_income, "description", None + model_benefits.basic_income, + "description", + None, ), parameter["subparams"]["basic_income"]["description"] -def test_stopped_parameter_values(test_client): +def test_stopped_parameter_values(test_client) -> None: response = test_client.get("/parameter/benefits/housing_allowance") parameter = json.loads(response.data) assert parameter["values"] == {"2016-12-01": None, "2010-01-01": 0.25} -def test_scale(test_client): +def test_scale(test_client) -> None: response = test_client.get("/parameter/taxes/social_security_contribution") parameter = json.loads(response.data) - assert sorted(list(parameter.keys())), [ + assert sorted(parameter.keys()), [ "brackets", "description", "id", @@ -135,7 +137,7 @@ def test_scale(test_client): } -def check_code(client, route, code): +def check_code(client, route, code) -> None: response = client.get(route) assert response.status_code == code @@ -153,10 +155,10 @@ def check_code(client, route, code): ("/parameter//taxes/income_tax_rate/", client.FOUND), ], ) -def test_routes_robustness(test_client, expected_code): +def test_routes_robustness(test_client, expected_code) -> None: check_code(test_client, *expected_code) -def test_parameter_encoding(test_client): +def test_parameter_encoding(test_client) -> None: parameter_response = test_client.get("/parameter/general/age_of_retirement") assert parameter_response.status_code == client.OK diff --git a/tests/web_api/test_spec.py b/tests/web_api/test_spec.py index 228cf27eb8..75a0f00e64 100644 --- a/tests/web_api/test_spec.py +++ b/tests/web_api/test_spec.py @@ -6,11 +6,11 @@ from openapi_spec_validator import OpenAPIV30SpecValidator -def assert_items_equal(x, y): +def assert_items_equal(x, y) -> None: assert sorted(x) == sorted(y) -def test_return_code(test_client): +def test_return_code(test_client) -> None: openAPI_response = test_client.get("/spec") assert openAPI_response.status_code == client.OK @@ -21,7 +21,7 @@ def body(test_client): return json.loads(openAPI_response.data.decode("utf-8")) -def test_paths(body): +def test_paths(body) -> None: assert_items_equal( body["paths"], [ @@ -37,29 +37,41 @@ def test_paths(body): ) -def test_entity_definition(body): +def test_entity_definition(body) -> None: assert "parents" in dpath.util.get(body, "components/schemas/Household/properties") assert "children" in dpath.util.get(body, "components/schemas/Household/properties") assert "salary" in dpath.util.get(body, "components/schemas/Person/properties") assert "rent" in dpath.util.get(body, "components/schemas/Household/properties") - assert "number" == dpath.util.get( - body, "components/schemas/Person/properties/salary/additionalProperties/type" + assert ( + dpath.util.get( + body, + "components/schemas/Person/properties/salary/additionalProperties/type", + ) + == "number" ) -def test_situation_definition(body): +def test_situation_definition(body) -> None: situation_input = body["components"]["schemas"]["SituationInput"] situation_output = body["components"]["schemas"]["SituationOutput"] for situation in situation_input, situation_output: assert "households" in dpath.util.get(situation, "/properties") assert "persons" in dpath.util.get(situation, "/properties") - assert "#/components/schemas/Household" == dpath.util.get( - situation, "/properties/households/additionalProperties/$ref" + assert ( + dpath.util.get( + situation, + "/properties/households/additionalProperties/$ref", + ) + == "#/components/schemas/Household" ) - assert "#/components/schemas/Person" == dpath.util.get( - situation, "/properties/persons/additionalProperties/$ref" + assert ( + dpath.util.get( + situation, + "/properties/persons/additionalProperties/$ref", + ) + == "#/components/schemas/Person" ) -def test_respects_spec(body): - assert not [error for error in OpenAPIV30SpecValidator(body).iter_errors()] +def test_respects_spec(body) -> None: + assert not list(OpenAPIV30SpecValidator(body).iter_errors()) diff --git a/tests/web_api/test_trace.py b/tests/web_api/test_trace.py index ee6c6ab21f..9463e69dfb 100644 --- a/tests/web_api/test_trace.py +++ b/tests/web_api/test_trace.py @@ -7,24 +7,28 @@ from openfisca_country_template.situation_examples import couple, single -def assert_items_equal(x, y): +def assert_items_equal(x, y) -> None: assert set(x) == set(y) -def test_trace_basic(test_client): +def test_trace_basic(test_client) -> None: simulation_json = json.dumps(single) response = test_client.post( - "/trace", data=simulation_json, content_type="application/json" + "/trace", + data=simulation_json, + content_type="application/json", ) assert response.status_code == client.OK response_json = json.loads(response.data.decode("utf-8")) disposable_income_value = dpath.util.get( - response_json, "trace/disposable_income<2017-01>/value" + response_json, + "trace/disposable_income<2017-01>/value", ) assert isinstance(disposable_income_value, list) assert isinstance(disposable_income_value[0], float) disposable_income_dep = dpath.util.get( - response_json, "trace/disposable_income<2017-01>/dependencies" + response_json, + "trace/disposable_income<2017-01>/dependencies", ) assert_items_equal( disposable_income_dep, @@ -36,29 +40,35 @@ def test_trace_basic(test_client): ], ) basic_income_dep = dpath.util.get( - response_json, "trace/basic_income<2017-01>/dependencies" + response_json, + "trace/basic_income<2017-01>/dependencies", ) assert_items_equal(basic_income_dep, ["age<2017-01>"]) -def test_trace_enums(test_client): +def test_trace_enums(test_client) -> None: new_single = copy.deepcopy(single) new_single["households"]["_"]["housing_occupancy_status"] = {"2017-01": None} simulation_json = json.dumps(new_single) response = test_client.post( - "/trace", data=simulation_json, content_type="application/json" + "/trace", + data=simulation_json, + content_type="application/json", ) response_json = json.loads(response.data) housing_status = dpath.util.get( - response_json, "trace/housing_occupancy_status<2017-01>/value" + response_json, + "trace/housing_occupancy_status<2017-01>/value", ) assert housing_status[0] == "tenant" # The default value -def test_entities_description(test_client): +def test_entities_description(test_client) -> None: simulation_json = json.dumps(couple) response = test_client.post( - "/trace", data=simulation_json, content_type="application/json" + "/trace", + data=simulation_json, + content_type="application/json", ) response_json = json.loads(response.data.decode("utf-8")) assert_items_equal( @@ -67,10 +77,12 @@ def test_entities_description(test_client): ) -def test_root_nodes(test_client): +def test_root_nodes(test_client) -> None: simulation_json = json.dumps(couple) response = test_client.post( - "/trace", data=simulation_json, content_type="application/json" + "/trace", + data=simulation_json, + content_type="application/json", ) response_json = json.loads(response.data.decode("utf-8")) assert_items_equal( @@ -83,25 +95,29 @@ def test_root_nodes(test_client): ) -def test_str_variable(test_client): +def test_str_variable(test_client) -> None: new_couple = copy.deepcopy(couple) new_couple["households"]["_"]["postal_code"] = {"2017-01": None} simulation_json = json.dumps(new_couple) response = test_client.post( - "/trace", data=simulation_json, content_type="application/json" + "/trace", + data=simulation_json, + content_type="application/json", ) assert response.status_code == client.OK -def test_trace_parameters(test_client): +def test_trace_parameters(test_client) -> None: new_couple = copy.deepcopy(couple) new_couple["households"]["_"]["housing_tax"] = {"2017": None} simulation_json = json.dumps(new_couple) response = test_client.post( - "/trace", data=simulation_json, content_type="application/json" + "/trace", + data=simulation_json, + content_type="application/json", ) response_json = json.loads(response.data.decode("utf-8")) diff --git a/tests/web_api/test_variables.py b/tests/web_api/test_variables.py index d53581618d..d3b46dfff9 100644 --- a/tests/web_api/test_variables.py +++ b/tests/web_api/test_variables.py @@ -5,7 +5,7 @@ import pytest -def assert_items_equal(x, y): +def assert_items_equal(x, y) -> None: assert set(x) == set(y) @@ -17,15 +17,14 @@ def assert_items_equal(x, y): @pytest.fixture(scope="module") def variables_response(test_client): - variables_response = test_client.get("/variables") - return variables_response + return test_client.get("/variables") -def test_return_code(variables_response): +def test_return_code(variables_response) -> None: assert variables_response.status_code == client.OK -def test_response_data(variables_response): +def test_response_data(variables_response) -> None: variables = json.loads(variables_response.data.decode("utf-8")) assert variables["birth"] == { "description": "Birth date", @@ -36,22 +35,21 @@ def test_response_data(variables_response): # /variable/ -def test_error_code_non_existing_variable(test_client): +def test_error_code_non_existing_variable(test_client) -> None: response = test_client.get("/variable/non_existing_variable") assert response.status_code == client.NOT_FOUND @pytest.fixture(scope="module") def input_variable_response(test_client): - input_variable_response = test_client.get("/variable/birth") - return input_variable_response + return test_client.get("/variable/birth") -def test_return_code_existing_input_variable(input_variable_response): +def test_return_code_existing_input_variable(input_variable_response) -> None: assert input_variable_response.status_code == client.OK -def check_input_variable_value(key, expected_value, input_variable=None): +def check_input_variable_value(key, expected_value, input_variable=None) -> None: assert input_variable[key] == expected_value @@ -66,25 +64,25 @@ def check_input_variable_value(key, expected_value, input_variable=None): ("references", ["https://en.wiktionary.org/wiki/birthdate"]), ], ) -def test_input_variable_value(expected_values, input_variable_response): +def test_input_variable_value(expected_values, input_variable_response) -> None: input_variable = json.loads(input_variable_response.data.decode("utf-8")) check_input_variable_value(*expected_values, input_variable=input_variable) -def test_input_variable_github_url(test_client): +def test_input_variable_github_url(test_client) -> None: input_variable_response = test_client.get("/variable/income_tax") input_variable = json.loads(input_variable_response.data.decode("utf-8")) assert re.match(GITHUB_URL_REGEX, input_variable["source"]) -def test_return_code_existing_variable(test_client): +def test_return_code_existing_variable(test_client) -> None: variable_response = test_client.get("/variable/income_tax") assert variable_response.status_code == client.OK -def check_variable_value(key, expected_value, variable=None): +def check_variable_value(key, expected_value, variable=None) -> None: assert variable[key] == expected_value @@ -98,19 +96,19 @@ def check_variable_value(key, expected_value, variable=None): ("entity", "person"), ], ) -def test_variable_value(expected_values, test_client): +def test_variable_value(expected_values, test_client) -> None: variable_response = test_client.get("/variable/income_tax") variable = json.loads(variable_response.data.decode("utf-8")) check_variable_value(*expected_values, variable=variable) -def test_variable_formula_github_link(test_client): +def test_variable_formula_github_link(test_client) -> None: variable_response = test_client.get("/variable/income_tax") variable = json.loads(variable_response.data.decode("utf-8")) assert re.match(GITHUB_URL_REGEX, variable["formulas"]["0001-01-01"]["source"]) -def test_variable_formula_content(test_client): +def test_variable_formula_content(test_client) -> None: variable_response = test_client.get("/variable/income_tax") variable = json.loads(variable_response.data.decode("utf-8")) content = variable["formulas"]["0001-01-01"]["content"] @@ -121,13 +119,13 @@ def test_variable_formula_content(test_client): ) -def test_null_values_are_dropped(test_client): +def test_null_values_are_dropped(test_client) -> None: variable_response = test_client.get("/variable/age") variable = json.loads(variable_response.data.decode("utf-8")) - assert "references" not in variable.keys() + assert "references" not in variable -def test_variable_with_start_and_stop_date(test_client): +def test_variable_with_start_and_stop_date(test_client) -> None: response = test_client.get("/variable/housing_allowance") variable = json.loads(response.data.decode("utf-8")) assert_items_equal(variable["formulas"], ["1980-01-01", "2016-12-01"]) @@ -135,12 +133,12 @@ def test_variable_with_start_and_stop_date(test_client): assert "formula" in variable["formulas"]["1980-01-01"]["content"] -def test_variable_with_enum(test_client): +def test_variable_with_enum(test_client) -> None: response = test_client.get("/variable/housing_occupancy_status") variable = json.loads(response.data.decode("utf-8")) assert variable["valueType"] == "String" assert variable["defaultValue"] == "tenant" - assert "possibleValues" in variable.keys() + assert "possibleValues" in variable assert variable["possibleValues"] == { "free_lodger": "Free lodger", "homeless": "Homeless", @@ -151,20 +149,19 @@ def test_variable_with_enum(test_client): @pytest.fixture(scope="module") def dated_variable_response(test_client): - dated_variable_response = test_client.get("/variable/basic_income") - return dated_variable_response + return test_client.get("/variable/basic_income") -def test_return_code_existing_dated_variable(dated_variable_response): +def test_return_code_existing_dated_variable(dated_variable_response) -> None: assert dated_variable_response.status_code == client.OK -def test_dated_variable_formulas_dates(dated_variable_response): +def test_dated_variable_formulas_dates(dated_variable_response) -> None: dated_variable = json.loads(dated_variable_response.data.decode("utf-8")) assert_items_equal(dated_variable["formulas"], ["2016-12-01", "2015-12-01"]) -def test_dated_variable_formulas_content(dated_variable_response): +def test_dated_variable_formulas_content(dated_variable_response) -> None: dated_variable = json.loads(dated_variable_response.data.decode("utf-8")) formula_code_2016 = dated_variable["formulas"]["2016-12-01"]["content"] formula_code_2015 = dated_variable["formulas"]["2015-12-01"]["content"] @@ -175,12 +172,12 @@ def test_dated_variable_formulas_content(dated_variable_response): assert "return" in formula_code_2015 -def test_variable_encoding(test_client): +def test_variable_encoding(test_client) -> None: variable_response = test_client.get("/variable/pension") assert variable_response.status_code == client.OK -def test_variable_documentation(test_client): +def test_variable_documentation(test_client) -> None: response = test_client.get("/variable/housing_allowance") variable = json.loads(response.data.decode("utf-8")) assert ( From d05b3aa2134974e23ccfe94445c204a43090ff72 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 26 Jun 2023 13:36:08 +0200 Subject: [PATCH 128/188] Bump version --- CHANGELOG.md | 6 +++ openfisca_core/entities/_core_entity.py | 6 +-- openfisca_core/entities/group_entity.py | 5 +-- openfisca_core/entities/helpers.py | 8 +--- openfisca_core/entities/role.py | 8 ++-- openfisca_core/entities/types.py | 6 +-- .../errors/situation_parsing_error.py | 5 +-- openfisca_core/holders/holder.py | 6 +-- openfisca_core/projectors/helpers.py | 11 ++---- openfisca_core/projectors/typing.py | 8 ++-- .../simulations/_build_from_variables.py | 7 +--- openfisca_core/simulations/_type_guards.py | 23 +++++------ openfisca_core/simulations/simulation.py | 10 ++--- .../simulations/simulation_builder.py | 39 +++++++++---------- openfisca_core/simulations/typing.py | 6 +-- .../taxbenefitsystems/tax_benefit_system.py | 13 +++---- openfisca_core/tools/test_runner.py | 10 ++--- openfisca_core/variables/helpers.py | 7 +--- openfisca_core/variables/variable.py | 7 ++-- pyproject.toml | 9 ----- setup.py | 7 ++-- tests/core/test_simulation_builder.py | 5 +-- 22 files changed, 80 insertions(+), 132 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ade8610231..002cf14714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.5.6 [#1185](https://github.com/openfisca/openfisca-core/pull/1185) + +#### Technical changes + +- Remove pre Python 3.9 syntax. + ### 41.5.5 [#1220](https://github.com/openfisca/openfisca-core/pull/1220) #### Technical changes diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index b0647fe50a..da3e6ea981 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -1,15 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import os from abc import abstractmethod +from . import types as t from .role import Role -if TYPE_CHECKING: - from . import types as t - class _CoreEntity: """Base class to build entities from.""" diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index a25352ba55..b47c92a525 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Iterable, Sequence import textwrap from itertools import chain @@ -9,9 +9,6 @@ from ._core_entity import _CoreEntity from .role import Role -if TYPE_CHECKING: - from collections.abc import Iterable, Sequence - class GroupEntity(_CoreEntity): """Represents an entity containing several others with different roles. diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 1091b9fb74..d5ba6cc6a0 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,15 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Iterable, Sequence +from . import types as t from .entity import Entity from .group_entity import GroupEntity -if TYPE_CHECKING: - from collections.abc import Iterable, Sequence - - from . import types as t - def build_entity( key: str, diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 9f28dc7aa8..45193fffc0 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,14 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from collections.abc import Iterable, Mapping +from typing import Any import dataclasses import textwrap -if TYPE_CHECKING: - from collections.abc import Iterable, Mapping - - from .types import SingleEntity +from .types import SingleEntity class Role: diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index a6bf5e189f..38607d5488 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -1,13 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NewType, Protocol +from collections.abc import Iterable +from typing import NewType, Protocol from typing_extensions import Required, TypedDict from openfisca_core import types as t -if TYPE_CHECKING: - from collections.abc import Iterable - # Entities #: For example "person". diff --git a/openfisca_core/errors/situation_parsing_error.py b/openfisca_core/errors/situation_parsing_error.py index 6563d1da2a..a5d7ee88d3 100644 --- a/openfisca_core/errors/situation_parsing_error.py +++ b/openfisca_core/errors/situation_parsing_error.py @@ -1,14 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Iterable import os import dpath.util -if TYPE_CHECKING: - from collections.abc import Iterable - class SituationParsingError(Exception): """Exception raised when the situation provided as an input for a simulation cannot be parsed.""" diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index 8e30b4bb2f..a8ddf3ed3a 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from collections.abc import Sequence +from typing import Any import os import warnings @@ -20,9 +21,6 @@ from .memory_usage import MemoryUsage -if TYPE_CHECKING: - from collections.abc import Sequence - class Holder: """A holder keeps tracks of a variable values after they have been calculated, or set as an input.""" diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index 1b3961c266..4c7712106a 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -1,15 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Mapping -from openfisca_core import entities, projectors - -if TYPE_CHECKING: - from collections.abc import Mapping +from openfisca_core.types import GroupEntity, Role, SingleEntity - from openfisca_core.types import GroupEntity, Role, SingleEntity +from openfisca_core import entities, projectors - from .typing import GroupPopulation, Population +from .typing import GroupPopulation, Population def projectable(function): diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py index 56e50e7230..a49bc96621 100644 --- a/openfisca_core/projectors/typing.py +++ b/openfisca_core/projectors/typing.py @@ -1,11 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Protocol +from collections.abc import Mapping +from typing import Protocol -if TYPE_CHECKING: - from collections.abc import Mapping - - from openfisca_core.types import GroupEntity, SingleEntity +from openfisca_core.types import GroupEntity, SingleEntity class Population(Protocol): diff --git a/openfisca_core/simulations/_build_from_variables.py b/openfisca_core/simulations/_build_from_variables.py index 5819a8aa2b..20f49ce113 100644 --- a/openfisca_core/simulations/_build_from_variables.py +++ b/openfisca_core/simulations/_build_from_variables.py @@ -2,17 +2,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING from typing_extensions import Self from openfisca_core import errors from ._build_default_simulation import _BuildDefaultSimulation from ._type_guards import is_variable_dated - -if TYPE_CHECKING: - from .simulation import Simulation - from .typing import Entity, Population, TaxBenefitSystem, Variables +from .simulation import Simulation +from .typing import Entity, Population, TaxBenefitSystem, Variables class _BuildFromVariables: diff --git a/openfisca_core/simulations/_type_guards.py b/openfisca_core/simulations/_type_guards.py index 8f31a7a2e8..990248213d 100644 --- a/openfisca_core/simulations/_type_guards.py +++ b/openfisca_core/simulations/_type_guards.py @@ -2,21 +2,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from collections.abc import Iterable from typing_extensions import TypeGuard -if TYPE_CHECKING: - from collections.abc import Iterable - - from .typing import ( - Axes, - DatedVariable, - FullySpecifiedEntities, - ImplicitGroupEntities, - Params, - UndatedVariable, - Variables, - ) +from .typing import ( + Axes, + DatedVariable, + FullySpecifiedEntities, + ImplicitGroupEntities, + Params, + UndatedVariable, + Variables, +) def are_entities_fully_specified( diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 259e676e36..c32fea22af 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,6 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple +from collections.abc import Mapping +from typing import NamedTuple + +from openfisca_core.types import Population, TaxBenefitSystem, Variable import tempfile import warnings @@ -16,11 +19,6 @@ warnings as core_warnings, ) -if TYPE_CHECKING: - from collections.abc import Mapping - - from openfisca_core.types import Population, TaxBenefitSystem, Variable - class Simulation: """Represents a simulation, and handles the calculation logic.""" diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 854e99e958..1ebb499239 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NoReturn +from collections.abc import Iterable, Sequence +from numpy.typing import NDArray as Array +from typing import NoReturn import copy @@ -19,26 +21,21 @@ has_axes, ) from .simulation import Simulation - -if TYPE_CHECKING: - from collections.abc import Iterable, Sequence - from numpy.typing import NDArray as Array - - from .typing import ( - Axis, - Entity, - FullySpecifiedEntities, - GroupEntities, - GroupEntity, - ImplicitGroupEntities, - Params, - ParamsWithoutAxes, - Population, - Role, - SingleEntity, - TaxBenefitSystem, - Variables, - ) +from .typing import ( + Axis, + Entity, + FullySpecifiedEntities, + GroupEntities, + GroupEntity, + ImplicitGroupEntities, + Params, + ParamsWithoutAxes, + Population, + Role, + SingleEntity, + TaxBenefitSystem, + Variables, +) class SimulationBuilder: diff --git a/openfisca_core/simulations/typing.py b/openfisca_core/simulations/typing.py index 8eda695384..8091994e53 100644 --- a/openfisca_core/simulations/typing.py +++ b/openfisca_core/simulations/typing.py @@ -3,7 +3,8 @@ from __future__ import annotations from collections.abc import Iterable, Sequence -from typing import TYPE_CHECKING, Protocol, TypeVar, TypedDict, Union +from numpy.typing import NDArray as Array +from typing import Protocol, TypeVar, TypedDict, Union from typing_extensions import NotRequired, Required, TypeAlias import datetime @@ -18,9 +19,6 @@ str_ as String, ) -if TYPE_CHECKING: - from numpy.typing import NDArray as Array - #: Generic type variables. E = TypeVar("E") G = TypeVar("G", covariant=True) diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 311143b412..8c48f64715 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -1,6 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from collections.abc import Sequence +from typing import Any + +from openfisca_core.types import ParameterNodeAtInstant import ast import copy @@ -17,6 +20,7 @@ import traceback from openfisca_core import commons, periods, variables +from openfisca_core.entities import Entity from openfisca_core.errors import VariableNameConflictError, VariableNotFoundError from openfisca_core.parameters import ParameterNode from openfisca_core.periods import Instant, Period @@ -24,13 +28,6 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable -if TYPE_CHECKING: - from collections.abc import Sequence - - from openfisca_core.types import ParameterNodeAtInstant - - from openfisca_core.entities import Entity - log = logging.getLogger(__name__) diff --git a/openfisca_core/tools/test_runner.py b/openfisca_core/tools/test_runner.py index 34e2d0a210..fcb5572b79 100644 --- a/openfisca_core/tools/test_runner.py +++ b/openfisca_core/tools/test_runner.py @@ -1,8 +1,11 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from collections.abc import Sequence +from typing import Any from typing_extensions import Literal, TypedDict +from openfisca_core.types import TaxBenefitSystem + import dataclasses import os import pathlib @@ -18,11 +21,6 @@ from openfisca_core.tools import assert_near from openfisca_core.warnings import LibYAMLWarning -if TYPE_CHECKING: - from collections.abc import Sequence - - from openfisca_core.types import TaxBenefitSystem - class Options(TypedDict, total=False): aggregate: bool diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index a9cd5df801..5038a78240 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -1,12 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - import sortedcontainers -if TYPE_CHECKING: - from openfisca_core import variables - from openfisca_core.periods import Period +from openfisca_core import variables +from openfisca_core.periods import Period def get_annualized_variable( diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index ac226dda2f..77411c32bb 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NoReturn +from typing import NoReturn + +from openfisca_core.types import Formula, Instant import datetime import re @@ -16,9 +18,6 @@ from . import config, helpers -if TYPE_CHECKING: - from openfisca_core.types import Formula, Instant - class Variable: """A `variable `_ of the legislation. diff --git a/pyproject.toml b/pyproject.toml index d44a6e119a..1e2a43ee4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,2 @@ [tool.black] target-version = ["py39", "py310", "py311"] - -[tool.ruff] -target-version = "py39" - -[tool.ruff.format] -docstring-code-format = true - -[tool.ruff.lint] -select = ["ALL"] diff --git a/setup.py b/setup.py index 2ddfd2c468..2f1c7089bf 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ "numpy >=1.24.2, <1.25", "pendulum >=2.1.2, <3.0.0", "psutil >=5.9.4, <6.0", - "pytest >=7.2.2, <8.0", + "pytest >=8.3.3, <9.0", "sortedcontainers >=2.4.0, <3.0", "typing_extensions >=4.5.0, <5.0", "StrEnum >=0.4.8, <0.5.0", # 3.11.x backport @@ -63,13 +63,14 @@ "pylint-per-file-ignores >=1.3.2, <2.0", "pyright >=1.1.381, <2.0", "ruff >=0.6.7, <1.0", + "ruff-lsp >=0.0.57, <1.0", "xdoctest >=1.2.0, <2.0", *api_requirements, ] setup( name="OpenFisca-Core", - version="41.5.5", + version="41.5.6", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ @@ -106,7 +107,7 @@ "dev": dev_requirements, "ci": [ "build >=0.10.0, <0.11.0", - "coveralls >=3.3.1, <4.0", + "coveralls >=4.0.1, <5.0", "twine >=5.1.1, <6.0", "wheel >=0.40.0, <0.41.0", ], diff --git a/tests/core/test_simulation_builder.py b/tests/core/test_simulation_builder.py index ec131920e2..b905b29b84 100644 --- a/tests/core/test_simulation_builder.py +++ b/tests/core/test_simulation_builder.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from collections.abc import Iterable import datetime @@ -15,9 +15,6 @@ from openfisca_core.tools import test_runner from openfisca_core.variables import Variable -if TYPE_CHECKING: - from collections.abc import Iterable - @pytest.fixture def int_variable(persons): From 257a68d4ebc30a9b8e94122bdd273a40981717f6 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 13:37:31 +0200 Subject: [PATCH 129/188] chore(deps): update pendulum --- openfisca_core/periods/period_.py | 8 ++++---- openfisca_tasks/lint.mk | 1 + setup.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 0dcf960bbf..14726d5360 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -348,14 +348,14 @@ def size_in_weeks(self): if self.unit == DateUnit.YEAR: start = self.start.date cease = start.add(years=self.size) - delta = pendulum.period(start, cease) - return delta.as_interval().weeks + delta = pendulum.interval(start, cease) + return delta.in_weeks() if self.unit == DateUnit.MONTH: start = self.start.date cease = start.add(months=self.size) - delta = pendulum.period(start, cease) - return delta.as_interval().weeks + delta = pendulum.interval(start, cease) + return delta.in_weeks() if self.unit == DateUnit.WEEK: return self.size diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 445abba10b..7f90aa5d58 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -42,6 +42,7 @@ check-types: @mypy \ openfisca_core/commons \ openfisca_core/entities \ + openfisca_core/periods \ openfisca_core/types.py @$(call print_pass,$@:) diff --git a/setup.py b/setup.py index 2f1c7089bf..6b573a28b1 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ "dpath >=2.1.4, <3.0", "numexpr >=2.8.4, <3.0", "numpy >=1.24.2, <1.25", - "pendulum >=2.1.2, <3.0.0", + "pendulum >=3.0.0, <4.0.0", "psutil >=5.9.4, <6.0", "pytest >=8.3.3, <9.0", "sortedcontainers >=2.4.0, <3.0", From 28dc8d501ce205a00dc8eb99b997baa85343fb43 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 18 Sep 2024 00:28:10 +0200 Subject: [PATCH 130/188] refactor(variables): remove rubn-time type check --- openfisca_core/periods/__init__.py | 49 +++++++++++++++++++++------- openfisca_core/periods/config.py | 9 ----- openfisca_core/periods/types.py | 0 openfisca_core/variables/variable.py | 9 +++-- 4 files changed, 42 insertions(+), 25 deletions(-) create mode 100644 openfisca_core/periods/types.py diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 4669c7ff4f..ca23dbf765 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -21,20 +21,14 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .config import ( # noqa: F401 - DAY, - ETERNITY, +from .config import ( INSTANT_PATTERN, - MONTH, - WEEK, - WEEKDAY, - YEAR, date_by_instant_cache, str_by_instant_cache, year_or_month_or_day_re, ) -from .date_unit import DateUnit # noqa: F401 -from .helpers import ( # noqa: F401 +from .date_unit import DateUnit +from .helpers import ( instant, instant_date, key_period_size, @@ -42,5 +36,38 @@ unit_weight, unit_weights, ) -from .instant_ import Instant # noqa: F401 -from .period_ import Period # noqa: F401 +from .instant_ import Instant +from .period_ import Period + +WEEKDAY = DateUnit.WEEKDAY +WEEK = DateUnit.WEEK +DAY = DateUnit.DAY +MONTH = DateUnit.MONTH +YEAR = DateUnit.YEAR +ETERNITY = DateUnit.ETERNITY +ISOFORMAT = DateUnit.isoformat +ISOCALENDAR = DateUnit.isocalendar + +__all__ = [ + "INSTANT_PATTERN", + "date_by_instant_cache", + "str_by_instant_cache", + "year_or_month_or_day_re", + "DateUnit", + "instant", + "instant_date", + "key_period_size", + "period", + "unit_weight", + "unit_weights", + "Instant", + "Period", + "WEEKDAY", + "WEEK", + "DAY", + "MONTH", + "YEAR", + "ETERNITY", + "ISOFORMAT", + "ISOCALENDAR", +] diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/config.py index 26ce30a5aa..bfe4107d9c 100644 --- a/openfisca_core/periods/config.py +++ b/openfisca_core/periods/config.py @@ -1,14 +1,5 @@ import re -from .date_unit import DateUnit - -WEEKDAY = DateUnit.WEEKDAY -WEEK = DateUnit.WEEK -DAY = DateUnit.DAY -MONTH = DateUnit.MONTH -YEAR = DateUnit.YEAR -ETERNITY = DateUnit.ETERNITY - # Matches "2015", "2015-01", "2015-01-01" # Does not match "2015-13", "2015-12-32" INSTANT_PATTERN = re.compile( diff --git a/openfisca_core/periods/types.py b/openfisca_core/periods/types.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 77411c32bb..d69fed5466 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -2,8 +2,6 @@ from typing import NoReturn -from openfisca_core.types import Formula, Instant - import datetime import re import textwrap @@ -12,6 +10,7 @@ import sortedcontainers from openfisca_core import periods, tools +from openfisca_core import types as t from openfisca_core.entities import Entity, GroupEntity from openfisca_core.indexed_enums import Enum, EnumArray from openfisca_core.periods import DateUnit, Period @@ -385,8 +384,8 @@ def get_introspection_data(cls): def get_formula( self, - period: Instant | Period | str | int = None, - ) -> Formula | None: + period: Union[t.Instant, t.Period, str, int] = None, + ) -> Optional[t.Formula]: """Returns the formula to compute the variable at the given period. If no period is given and the variable has several formulas, the method @@ -399,7 +398,7 @@ def get_formula( Formula used to compute the variable. """ - instant: Instant | None + instant: Optional[t.Instant] if not self.formulas: return None From c745a221a9ed33f285f787b394a5e766fb9acdc5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 18 Sep 2024 00:28:39 +0200 Subject: [PATCH 131/188] refactor(populations): remove run-time type check --- openfisca_core/populations/population.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index f9eee1c2a2..94cfa95d14 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -84,8 +84,8 @@ def check_period_validity( variable_name: str, period: int | str | Period | None, ) -> None: - if isinstance(period, (int, str, Period)): - return + if isinstance(period, (int, str, periods.Period)): + return None stack = traceback.extract_stack() filename, line_number, function_name, line_of_code = stack[-3] From dd71acfb470b20477ce404ac28080c67fe975a2d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 18 Sep 2024 00:30:21 +0200 Subject: [PATCH 132/188] refactor(types): do not use abstract classes --- openfisca_core/types.py | 68 +++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/openfisca_core/types.py b/openfisca_core/types.py index 16a1f0e90e..aae0a0540b 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -1,13 +1,10 @@ from __future__ import annotations -import typing_extensions -from collections.abc import Sequence +from collections.abc import Iterable, Sequence, Sized from numpy.typing import NDArray -from typing import Any, TypeVar +from typing import Any, TypeVar, Union from typing_extensions import Protocol, TypeAlias -import abc - import numpy N = TypeVar("N", bound=numpy.generic, covariant=True) @@ -26,6 +23,10 @@ #: Type variable representing a value. A = TypeVar("A", covariant=True) +#: Generic type vars. +T_cov = TypeVar("T_cov", covariant=True) +T_con = TypeVar("T_con", contravariant=True) + # Entities @@ -40,7 +41,6 @@ def check_role_validity(self, role: Any) -> None: ... @abc.abstractmethod def check_variable_defined_for_entity(self, variable_name: Any) -> None: ... - @abc.abstractmethod def get_variable( self, variable_name: Any, @@ -77,25 +77,35 @@ def get_memory_usage(self) -> Any: ... # Parameters -@typing_extensions.runtime_checkable -class ParameterNodeAtInstant(Protocol): ... +class ParameterNodeAtInstant(Protocol): + ... # Periods -class Instant(Protocol): ... +class Container(Protocol[T_con]): + def __contains__(self, item: T_con, /) -> bool: + ... -@typing_extensions.runtime_checkable -class Period(Protocol): - @property - @abc.abstractmethod - def start(self) -> Any: ... +class Indexable(Protocol[T_cov]): + def __getitem__(self, index: int, /) -> T_cov: + ... + + +class DateUnit(Container[str], Protocol): + ... + + +class Instant(Indexable[int], Iterable[int], Sized, Protocol): + ... + +class Period(Indexable[Union[DateUnit, Instant, int]], Protocol): @property - @abc.abstractmethod - def unit(self) -> Any: ... + def unit(self) -> DateUnit: + ... # Populations @@ -104,25 +114,25 @@ def unit(self) -> Any: ... class Population(Protocol): entity: Any - @abc.abstractmethod - def get_holder(self, variable_name: Any) -> Any: ... + def get_holder(self, variable_name: Any) -> Any: + ... # Simulations class Simulation(Protocol): - @abc.abstractmethod - def calculate(self, variable_name: Any, period: Any) -> Any: ... + def calculate(self, variable_name: Any, period: Any) -> Any: + ... - @abc.abstractmethod - def calculate_add(self, variable_name: Any, period: Any) -> Any: ... + def calculate_add(self, variable_name: Any, period: Any) -> Any: + ... - @abc.abstractmethod - def calculate_divide(self, variable_name: Any, period: Any) -> Any: ... + def calculate_divide(self, variable_name: Any, period: Any) -> Any: + ... - @abc.abstractmethod - def get_population(self, plural: Any | None) -> Any: ... + def get_population(self, plural: Any | None) -> Any: + ... # Tax-Benefit systems @@ -131,7 +141,6 @@ def get_population(self, plural: Any | None) -> Any: ... class TaxBenefitSystem(Protocol): person_entity: Any - @abc.abstractmethod def get_variable( self, variable_name: Any, @@ -148,7 +157,6 @@ class Variable(Protocol): class Formula(Protocol): - @abc.abstractmethod def __call__( self, population: Population, @@ -158,5 +166,5 @@ def __call__( class Params(Protocol): - @abc.abstractmethod - def __call__(self, instant: Instant) -> ParameterNodeAtInstant: ... + def __call__(self, instant: Instant) -> ParameterNodeAtInstant: + ... From c618f269e0ef4e3f1d411ffe9572a6d88d32a320 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 18 Sep 2024 00:33:04 +0200 Subject: [PATCH 133/188] doc(periods): add types --- openfisca_core/periods/__init__.py | 2 + openfisca_core/periods/_parsers.py | 5 +- openfisca_core/periods/config.py | 8 +- openfisca_core/periods/date_unit.py | 6 +- openfisca_core/periods/helpers.py | 68 ++++++---- openfisca_core/periods/instant_.py | 37 ++--- openfisca_core/periods/period_.py | 127 +++++++++--------- .../periods/tests/helpers/test_helpers.py | 14 +- .../periods/tests/helpers/test_period.py | 16 +-- openfisca_core/periods/types.py | 60 +++++++++ 10 files changed, 221 insertions(+), 122 deletions(-) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index ca23dbf765..1f2e68acaf 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -21,6 +21,7 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports +from . import types from .config import ( INSTANT_PATTERN, date_by_instant_cache, @@ -70,4 +71,5 @@ "ETERNITY", "ISOFORMAT", "ISOCALENDAR", + "types", ] diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index 95a17fb041..d1b93d4fb2 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -6,6 +6,7 @@ from pendulum.datetime import Date from pendulum.parsing import ParserError +from . import types as t from .date_unit import DateUnit from .instant_ import Instant from .period_ import Period @@ -13,7 +14,7 @@ invalid_week = re.compile(r".*(W[1-9]|W[1-9]-[0-9]|W[0-5][0-9]-0)$") -def _parse_period(value: str) -> Optional[Period]: +def _parse_period(value: str) -> Optional[t.Period]: """Parses ISO format/calendar periods. Such as "2012" or "2015-03". @@ -56,7 +57,7 @@ def _parse_period(value: str) -> Optional[Period]: return Period((unit, instant, 1)) -def _parse_unit(value: str) -> DateUnit: +def _parse_unit(value: str) -> t.DateUnit: """Determine the date unit of a date string. Args: diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/config.py index bfe4107d9c..9d3b3c008e 100644 --- a/openfisca_core/periods/config.py +++ b/openfisca_core/periods/config.py @@ -1,13 +1,17 @@ import re +from pendulum.datetime import Date + +from . import types as t + # Matches "2015", "2015-01", "2015-01-01" # Does not match "2015-13", "2015-12-32" INSTANT_PATTERN = re.compile( r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$", ) -date_by_instant_cache: dict = {} -str_by_instant_cache: dict = {} +date_by_instant_cache: dict[t.Instant, Date] = {} +str_by_instant_cache: dict[t.Instant, t.InstantStr] = {} year_or_month_or_day_re = re.compile( r"(18|19|20)\d{2}(-(0?[1-9]|1[0-2])(-([0-2]?\d|3[0-1]))?)?$", ) diff --git a/openfisca_core/periods/date_unit.py b/openfisca_core/periods/date_unit.py index 61f7fbc66f..7838133624 100644 --- a/openfisca_core/periods/date_unit.py +++ b/openfisca_core/periods/date_unit.py @@ -4,10 +4,12 @@ from strenum import StrEnum +from . import types as t + class DateUnitMeta(EnumMeta): @property - def isoformat(cls) -> tuple[DateUnit, ...]: + def isoformat(self) -> tuple[t.DateUnit, ...]: """Creates a :obj:`tuple` of ``key`` with isoformat items. Returns: @@ -27,7 +29,7 @@ def isoformat(cls) -> tuple[DateUnit, ...]: return DateUnit.DAY, DateUnit.MONTH, DateUnit.YEAR @property - def isocalendar(cls) -> tuple[DateUnit, ...]: + def isocalendar(self) -> tuple[t.DateUnit, ...]: """Creates a :obj:`tuple` of ``key`` with isocalendar items. Returns: diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index c1ccc4a3a2..e26be20281 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -1,4 +1,6 @@ -from typing import NoReturn, Optional +from __future__ import annotations + +from typing import NoReturn, Optional, overload import datetime import os @@ -7,12 +9,23 @@ from pendulum.parsing import ParserError from . import _parsers, config +from . import types as t from .date_unit import DateUnit from .instant_ import Instant from .period_ import Period -def instant(instant) -> Optional[Instant]: +@overload +def instant(instant: None) -> None: + ... + + +@overload +def instant(instant: object) -> Instant: + ... + + +def instant(instant: object | None) -> t.Instant | None: """Build a new instant, aka a triple of integers (year, month, day). Args: @@ -48,6 +61,9 @@ def instant(instant) -> Optional[Instant]: Instant((2021, 1, 1)) """ + + result: t.Instant | tuple[int, ...] + if instant is None: return None if isinstance(instant, Instant): @@ -58,27 +74,28 @@ def instant(instant) -> Optional[Instant]: raise ValueError( msg, ) - instant = Instant(int(fragment) for fragment in instant.split("-", 2)[:3]) + result = Instant(int(fragment) for fragment in instant.split("-", 2)[:3]) elif isinstance(instant, datetime.date): - instant = Instant((instant.year, instant.month, instant.day)) + result = Instant((instant.year, instant.month, instant.day)) elif isinstance(instant, int): - instant = (instant,) + result = (instant,) elif isinstance(instant, list): assert 1 <= len(instant) <= 3 - instant = tuple(instant) + result = tuple(instant) elif isinstance(instant, Period): - instant = instant.start + result = instant.start else: assert isinstance(instant, tuple), instant assert 1 <= len(instant) <= 3 - if len(instant) == 1: - return Instant((instant[0], 1, 1)) - if len(instant) == 2: - return Instant((instant[0], instant[1], 1)) - return Instant(instant) + result = instant + if len(result) == 1: + return Instant((result[0], 1, 1)) + if len(result) == 2: + return Instant((result[0], result[1], 1)) + return Instant(result) -def instant_date(instant: Optional[Instant]) -> Optional[datetime.date]: +def instant_date(instant: Optional[t.Instant]) -> Optional[datetime.date]: """Returns the date representation of an :class:`.Instant`. Args: @@ -104,7 +121,7 @@ def instant_date(instant: Optional[Instant]) -> Optional[datetime.date]: return instant_date -def period(value) -> Period: +def period(value: object) -> t.Period: """Build a new period, aka a triple (unit, start_instant, size). Args: @@ -124,7 +141,7 @@ def period(value) -> Period: Period((, Instant((2021, 1, 1)), 1)) >>> period(DateUnit.ETERNITY) - Period((, Instant((1, 1, 1)), inf)) + Period((, Instant((1, 1, 1)), 0)) >>> period(2021) Period((, Instant((2021, 1, 1)), 1)) @@ -162,15 +179,9 @@ def period(value) -> Period: return Period((DateUnit.DAY, instant(value), 1)) # We return an "eternity-period", for example - # ``, inf))>``. + # ``, 0))>``. if str(value).lower() == DateUnit.ETERNITY: - return Period( - ( - DateUnit.ETERNITY, - instant(datetime.date.min), - float("inf"), - ), - ) + return Period.eternity() # For example ``2021`` gives # ``, 1))>``. @@ -179,7 +190,7 @@ def period(value) -> Period: # Up to this point, if ``value`` is not a :obj:`str`, we desist. if not isinstance(value, str): - _raise_error(value) + _raise_error(str(value)) # There can't be empty strings. if not value: @@ -264,7 +275,7 @@ def _raise_error(value: str) -> NoReturn: raise ValueError(message) -def key_period_size(period: Period) -> str: +def key_period_size(period: t.Period) -> str: """Define a key in order to sort periods by length. It uses two aspects: first, ``unit``, then, ``size``. @@ -287,12 +298,11 @@ def key_period_size(period: Period) -> str: '300_3' """ - unit, start, size = period - return f"{unit_weight(unit)}_{size}" + return f"{unit_weight(period.unit)}_{period.size}" -def unit_weights() -> dict[str, int]: +def unit_weights() -> dict[t.DateUnit, int]: """Assign weights to date units. Examples: @@ -310,7 +320,7 @@ def unit_weights() -> dict[str, int]: } -def unit_weight(unit: str) -> int: +def unit_weight(unit: t.DateUnit) -> int: """Retrieves a specific date unit weight. Examples: diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 5042209492..8cfc95ef4b 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,10 +1,14 @@ +from __future__ import annotations + import pendulum +from pendulum.datetime import Date from . import config +from . import types as t from .date_unit import DateUnit -class Instant(tuple): +class Instant(tuple[int, int, int]): """An instant in time (year, month, day). An :class:`.Instant` represents the most atomic and indivisible @@ -13,10 +17,6 @@ class Instant(tuple): Current implementation considers this unit to be a day, so :obj:`instants <.Instant>` can be thought of as "day dates". - Args: - (tuple(tuple(int, int, int))): - The ``year``, ``month``, and ``day``, accordingly. - Examples: >>> instant = Instant((2021, 9, 13)) @@ -81,32 +81,38 @@ class Instant(tuple): def __repr__(self) -> str: return f"{self.__class__.__name__}({super().__repr__()})" - def __str__(self) -> str: + def __str__(self) -> t.InstantStr: instant_str = config.str_by_instant_cache.get(self) if instant_str is None: - config.str_by_instant_cache[self] = instant_str = self.date.isoformat() + instant_str = t.InstantStr(self.date.isoformat()) + config.str_by_instant_cache[self] = instant_str return instant_str @property - def date(self): + def date(self) -> Date: instant_date = config.date_by_instant_cache.get(self) if instant_date is None: - config.date_by_instant_cache[self] = instant_date = pendulum.date(*self) + instant_date = pendulum.date(*self) + config.date_by_instant_cache[self] = instant_date return instant_date @property - def day(self): + def day(self) -> int: return self[2] @property - def month(self): + def month(self) -> int: return self[1] - def offset(self, offset, unit): + @property + def year(self) -> int: + return self[0] + + def offset(self, offset: str | int, unit: t.DateUnit) -> t.Instant | None: """Increments/decrements the given instant with offset units. Args: @@ -195,6 +201,7 @@ def offset(self, offset, unit): return self.__class__((date.year, date.month, date.day)) return None - @property - def year(self): - return self[0] + @classmethod + def eternity(cls) -> t.Instant: + """Return an eternity instant.""" + return cls((1, 1, 1)) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 14726d5360..ad2fe62622 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,6 +1,6 @@ from __future__ import annotations -import typing +from collections.abc import Sequence import calendar import datetime @@ -8,16 +8,12 @@ import pendulum from . import helpers +from . import types as t from .date_unit import DateUnit from .instant_ import Instant -if typing.TYPE_CHECKING: - from collections.abc import Sequence - from pendulum.datetime import Date - - -class Period(tuple): +class Period(tuple[t.DateUnit, t.Instant, int]): """Toolbox to handle date intervals. A :class:`.Period` is a triple (``unit``, ``start``, ``size``). @@ -125,11 +121,11 @@ class Period(tuple): def __repr__(self) -> str: return f"{self.__class__.__name__}({super().__repr__()})" - def __str__(self) -> str: + def __str__(self) -> t.PeriodStr: unit, start_instant, size = self if unit == DateUnit.ETERNITY: - return unit.upper() + return t.PeriodStr(unit.upper()) # ISO format date units. f_year, month, day = start_instant @@ -141,56 +137,56 @@ def __str__(self) -> str: if unit == DateUnit.MONTH and size == 12 or unit == DateUnit.YEAR and size == 1: if month == 1: # civil year starting from january - return str(f_year) - # rolling year - return f"{DateUnit.YEAR}:{f_year}-{month:02d}" + return t.PeriodStr(str(f_year)) + # rolling year + return t.PeriodStr(f"{DateUnit.YEAR}:{f_year}-{month:02d}") # simple month if unit == DateUnit.MONTH and size == 1: - return f"{f_year}-{month:02d}" + return t.PeriodStr(f"{f_year}-{month:02d}") # several civil years if unit == DateUnit.YEAR and month == 1: - return f"{unit}:{f_year}:{size}" + return t.PeriodStr(f"{unit}:{f_year}:{size}") if unit == DateUnit.DAY: if size == 1: - return f"{f_year}-{month:02d}-{day:02d}" - return f"{unit}:{f_year}-{month:02d}-{day:02d}:{size}" + return t.PeriodStr(f"{f_year}-{month:02d}-{day:02d}") + return t.PeriodStr(f"{unit}:{f_year}-{month:02d}-{day:02d}:{size}") # 1 week if unit == DateUnit.WEEK and size == 1: if week < 10: - return f"{c_year}-W0{week}" + return t.PeriodStr(f"{c_year}-W0{week}") - return f"{c_year}-W{week}" + return t.PeriodStr(f"{c_year}-W{week}") # several weeks if unit == DateUnit.WEEK and size > 1: if week < 10: - return f"{unit}:{c_year}-W0{week}:{size}" + return t.PeriodStr(f"{unit}:{c_year}-W0{week}:{size}") - return f"{unit}:{c_year}-W{week}:{size}" + return t.PeriodStr(f"{unit}:{c_year}-W{week}:{size}") # 1 weekday if unit == DateUnit.WEEKDAY and size == 1: if week < 10: - return f"{c_year}-W0{week}-{weekday}" + return t.PeriodStr(f"{c_year}-W0{week}-{weekday}") - return f"{c_year}-W{week}-{weekday}" + return t.PeriodStr(f"{c_year}-W{week}-{weekday}") # several weekdays if unit == DateUnit.WEEKDAY and size > 1: if week < 10: - return f"{unit}:{c_year}-W0{week}-{weekday}:{size}" + return t.PeriodStr(f"{unit}:{c_year}-W0{week}-{weekday}:{size}") - return f"{unit}:{c_year}-W{week}-{weekday}:{size}" + return t.PeriodStr(f"{unit}:{c_year}-W{week}-{weekday}:{size}") # complex period - return f"{unit}:{f_year}-{month:02d}:{size}" + return t.PeriodStr(f"{unit}:{f_year}-{month:02d}:{size}") @property - def unit(self) -> str: + def unit(self) -> t.DateUnit: """The ``unit`` of the ``Period``. Example: @@ -203,7 +199,7 @@ def unit(self) -> str: return self[0] @property - def start(self) -> Instant: + def start(self) -> t.Instant: """The ``Instant`` at which the ``Period`` starts. Example: @@ -330,7 +326,7 @@ def size_in_days(self) -> int: raise ValueError(msg) @property - def size_in_weeks(self): + def size_in_weeks(self) -> int: """The ``size`` of the ``Period`` in weeks. Examples: @@ -348,13 +344,13 @@ def size_in_weeks(self): if self.unit == DateUnit.YEAR: start = self.start.date cease = start.add(years=self.size) - delta = pendulum.interval(start, cease) + delta = start.diff(cease) return delta.in_weeks() if self.unit == DateUnit.MONTH: start = self.start.date cease = start.add(months=self.size) - delta = pendulum.interval(start, cease) + delta = start.diff(cease) return delta.in_weeks() if self.unit == DateUnit.WEEK: @@ -364,7 +360,7 @@ def size_in_weeks(self): raise ValueError(msg) @property - def size_in_weekdays(self): + def size_in_weekdays(self) -> int: """The ``size`` of the ``Period`` in weekdays. Examples: @@ -396,11 +392,13 @@ def size_in_weekdays(self): raise ValueError(msg) @property - def days(self): + def days(self) -> int: """Same as ``size_in_days``.""" return (self.stop.date - self.start.date).days + 1 - def intersection(self, start, stop): + def intersection( + self, start: t.Instant | None, stop: t.Instant | None + ) -> t.Period | None: if start is None and stop is None: return self period_start = self[1] @@ -453,7 +451,7 @@ def intersection(self, start, stop): ), ) - def get_subperiods(self, unit: DateUnit) -> Sequence[Period]: + def get_subperiods(self, unit: t.DateUnit) -> Sequence[t.Period]: """Return the list of periods of unit ``unit`` contained in self. Examples: @@ -499,7 +497,7 @@ def get_subperiods(self, unit: DateUnit) -> Sequence[Period]: msg = f"Cannot subdivide {self.unit} into {unit}" raise ValueError(msg) - def offset(self, offset, unit=None): + def offset(self, offset: str | int, unit: t.DateUnit | None = None) -> t.Period: """Increment (or decrement) the given period with offset units. Examples: @@ -730,7 +728,7 @@ def offset(self, offset, unit=None): ), ) - def contains(self, other: Period) -> bool: + def contains(self, other: t.Period) -> bool: """Returns ``True`` if the period contains ``other``. For instance, ``period(2015)`` contains ``period(2015-01)``. @@ -739,7 +737,7 @@ def contains(self, other: Period) -> bool: return self.start <= other.start and self.stop >= other.stop @property - def stop(self) -> Instant: + def stop(self) -> t.Instant: """Return the last day of the period as an Instant instance. Examples: @@ -775,7 +773,7 @@ def stop(self) -> Instant: year, month, day = start_instant if unit == DateUnit.ETERNITY: - return Instant((float("inf"), float("inf"), float("inf"))) + return Instant.eternity() if unit == DateUnit.YEAR: date = start_instant.date.add(years=size, days=-1) @@ -798,67 +796,72 @@ def stop(self) -> Instant: # Reference periods @property - def last_week(self) -> Period: + def last_week(self) -> t.Period: return self.first_week.offset(-1) @property - def last_fortnight(self) -> Period: - start: Instant = self.first_week.start + def last_fortnight(self) -> t.Period: + start: t.Instant = self.first_week.start return self.__class__((DateUnit.WEEK, start, 1)).offset(-2) @property - def last_2_weeks(self) -> Period: - start: Instant = self.first_week.start + def last_2_weeks(self) -> t.Period: + start: t.Instant = self.first_week.start return self.__class__((DateUnit.WEEK, start, 2)).offset(-2) @property - def last_26_weeks(self) -> Period: - start: Instant = self.first_week.start + def last_26_weeks(self) -> t.Period: + start: t.Instant = self.first_week.start return self.__class__((DateUnit.WEEK, start, 26)).offset(-26) @property - def last_52_weeks(self) -> Period: - start: Instant = self.first_week.start + def last_52_weeks(self) -> t.Period: + start: t.Instant = self.first_week.start return self.__class__((DateUnit.WEEK, start, 52)).offset(-52) @property - def last_month(self) -> Period: + def last_month(self) -> t.Period: return self.first_month.offset(-1) @property - def last_3_months(self) -> Period: - start: Instant = self.first_month.start + def last_3_months(self) -> t.Period: + start: t.Instant = self.first_month.start return self.__class__((DateUnit.MONTH, start, 3)).offset(-3) @property - def last_year(self) -> Period: - start: Instant = self.start.offset("first-of", DateUnit.YEAR) + def last_year(self) -> t.Period: + start: t.Instant = self.start.offset("first-of", DateUnit.YEAR) return self.__class__((DateUnit.YEAR, start, 1)).offset(-1) @property - def n_2(self) -> Period: - start: Instant = self.start.offset("first-of", DateUnit.YEAR) + def n_2(self) -> t.Period: + start: t.Instant = self.start.offset("first-of", DateUnit.YEAR) return self.__class__((DateUnit.YEAR, start, 1)).offset(-2) @property - def this_year(self) -> Period: - start: Instant = self.start.offset("first-of", DateUnit.YEAR) + def this_year(self) -> t.Period: + start: t.Instant = self.start.offset("first-of", DateUnit.YEAR) return self.__class__((DateUnit.YEAR, start, 1)) @property - def first_month(self) -> Period: - start: Instant = self.start.offset("first-of", DateUnit.MONTH) + def first_month(self) -> t.Period: + start: t.Instant = self.start.offset("first-of", DateUnit.MONTH) return self.__class__((DateUnit.MONTH, start, 1)) @property - def first_day(self) -> Period: + def first_day(self) -> t.Period: return self.__class__((DateUnit.DAY, self.start, 1)) @property - def first_week(self) -> Period: - start: Instant = self.start.offset("first-of", DateUnit.WEEK) + def first_week(self) -> t.Period: + start: t.Instant = self.start.offset("first-of", DateUnit.WEEK) return self.__class__((DateUnit.WEEK, start, 1)) @property - def first_weekday(self) -> Period: + def first_weekday(self) -> t.Period: return self.__class__((DateUnit.WEEKDAY, self.start, 1)) + + @classmethod + def eternity(cls) -> t.Period: + """Return an eternity period.""" + return cls((DateUnit.ETERNITY, Instant.eternity(), 0)) diff --git a/openfisca_core/periods/tests/helpers/test_helpers.py b/openfisca_core/periods/tests/helpers/test_helpers.py index 3cbf078a2e..8998298685 100644 --- a/openfisca_core/periods/tests/helpers/test_helpers.py +++ b/openfisca_core/periods/tests/helpers/test_helpers.py @@ -47,9 +47,19 @@ def test_instant_date_with_an_invalid_argument(arg, error) -> None: (Period((DateUnit.MONTH, Instant((1, 1, 1)), 12)), "200_12"), (Period((DateUnit.YEAR, Instant((1, 1, 1)), 2)), "300_2"), (Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"), - ((DateUnit.DAY, None, 1), "100_1"), - ((DateUnit.MONTH, None, -1000), "200_-1000"), ], ) def test_key_period_size(arg, expected) -> None: assert periods.key_period_size(arg) == expected + + +@pytest.mark.parametrize( + "arg, error", + [ + [(DateUnit.DAY, None, 1), AttributeError], + [(DateUnit.MONTH, None, -1000), AttributeError], + ], +) +def test_key_period_size_when_an_invalid_argument(arg, error): + with pytest.raises(error): + periods.key_period_size(arg) diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py index c31e54c2ca..2db3865b31 100644 --- a/openfisca_core/periods/tests/helpers/test_period.py +++ b/openfisca_core/periods/tests/helpers/test_period.py @@ -9,15 +9,15 @@ @pytest.mark.parametrize( ("arg", "expected"), [ - ("eternity", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf")))), - ("ETERNITY", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf")))), - ( + ["eternity", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0))], + ["ETERNITY", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0))], + [ DateUnit.ETERNITY, - Period((DateUnit.ETERNITY, Instant((1, 1, 1)), float("inf"))), - ), - (datetime.date(1, 1, 1), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))), - (Instant((1, 1, 1)), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))), - ( + Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0)), + ], + [datetime.date(1, 1, 1), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))], + [Instant((1, 1, 1)), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))], + [ Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), ), diff --git a/openfisca_core/periods/types.py b/openfisca_core/periods/types.py index e69de29bb2..f8136f5b6b 100644 --- a/openfisca_core/periods/types.py +++ b/openfisca_core/periods/types.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from typing import NewType, Protocol + +from pendulum.datetime import Date + +from openfisca_core import types as t + +# New types. + +#: For example "2000-01". +InstantStr = NewType("InstantStr", str) + +#: For example "1:2000-01-01:day". +PeriodStr = NewType("PeriodStr", str) + + +# Periods + + +class DateUnit(t.DateUnit, Protocol): + ... + + +class Instant(t.Instant, Protocol): + @property + def year(self) -> int: + ... + + @property + def month(self) -> int: + ... + + @property + def day(self) -> int: + ... + + @property + def date(self) -> Date: + ... + + def offset(self, offset: str | int, unit: DateUnit) -> Instant | None: + ... + + +class Period(t.Period, Protocol): + @property + def size(self) -> int: + ... + + @property + def start(self) -> Instant: + ... + + @property + def stop(self) -> Instant: + ... + + def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: + ... From 07c93b9282c8b9be9c3517fa6a8bbf4aab748658 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 13:08:51 +0200 Subject: [PATCH 134/188] fix(periods): unhandled none --- openfisca_core/periods/period_.py | 34 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index ad2fe62622..e72a983e38 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -313,7 +313,12 @@ def size_in_days(self) -> int: """ if self.unit in (DateUnit.YEAR, DateUnit.MONTH): - last_day = self.start.offset(self.size, self.unit).offset(-1, DateUnit.DAY) + last = self.start.offset(self.size, self.unit) + if last is None: + raise NotImplementedError + last_day = last.offset(-1, DateUnit.DAY) + if last_day is None: + raise NotImplementedError return (last_day.date - self.start.date).days + 1 if self.unit == DateUnit.WEEK: @@ -379,7 +384,12 @@ def size_in_weekdays(self) -> int: return self.size_in_weeks * 7 if self.unit in DateUnit.MONTH: - last_day = self.start.offset(self.size, self.unit).offset(-1, DateUnit.DAY) + last = self.start.offset(self.size, self.unit) + if last is None: + raise NotImplementedError + last_day = last.offset(-1, DateUnit.DAY) + if last_day is None: + raise NotImplementedError return (last_day.date - self.start.date).days + 1 if self.unit == DateUnit.WEEK: @@ -830,22 +840,30 @@ def last_3_months(self) -> t.Period: @property def last_year(self) -> t.Period: - start: t.Instant = self.start.offset("first-of", DateUnit.YEAR) + start: None | t.Instant = self.start.offset("first-of", DateUnit.YEAR) + if start is None: + raise NotImplementedError return self.__class__((DateUnit.YEAR, start, 1)).offset(-1) @property def n_2(self) -> t.Period: - start: t.Instant = self.start.offset("first-of", DateUnit.YEAR) + start: None | t.Instant = self.start.offset("first-of", DateUnit.YEAR) + if start is None: + raise NotImplementedError return self.__class__((DateUnit.YEAR, start, 1)).offset(-2) @property def this_year(self) -> t.Period: - start: t.Instant = self.start.offset("first-of", DateUnit.YEAR) + start: None | t.Instant = self.start.offset("first-of", DateUnit.YEAR) + if start is None: + raise NotImplementedError return self.__class__((DateUnit.YEAR, start, 1)) @property def first_month(self) -> t.Period: - start: t.Instant = self.start.offset("first-of", DateUnit.MONTH) + start: None | t.Instant = self.start.offset("first-of", DateUnit.MONTH) + if start is None: + raise NotImplementedError return self.__class__((DateUnit.MONTH, start, 1)) @property @@ -854,7 +872,9 @@ def first_day(self) -> t.Period: @property def first_week(self) -> t.Period: - start: t.Instant = self.start.offset("first-of", DateUnit.WEEK) + start: None | t.Instant = self.start.offset("first-of", DateUnit.WEEK) + if start is None: + raise NotImplementedError return self.__class__((DateUnit.WEEK, start, 1)) @property From 9bd5c7135e6c880ac1fa10cdf66d3dcfcc9f98eb Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 13:53:59 +0200 Subject: [PATCH 135/188] fix(periods): dunder method types --- openfisca_core/periods/date_unit.py | 5 +++++ openfisca_core/periods/instant_.py | 10 ++++++++++ openfisca_core/periods/period_.py | 12 ++++++++++-- openfisca_core/periods/types.py | 6 ++++++ openfisca_core/types.py | 10 +++------- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/openfisca_core/periods/date_unit.py b/openfisca_core/periods/date_unit.py index 7838133624..468b4b0b4b 100644 --- a/openfisca_core/periods/date_unit.py +++ b/openfisca_core/periods/date_unit.py @@ -96,6 +96,11 @@ class DateUnit(StrEnum, metaclass=DateUnitMeta): """ + def __contains__(self, other: object) -> bool: + if isinstance(other, str): + return super().__contains__(other) + return NotImplemented + WEEKDAY = "weekday" WEEK = "week" DAY = "day" diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 8cfc95ef4b..fd866bc022 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -90,6 +90,16 @@ def __str__(self) -> t.InstantStr: return instant_str + def __lt__(self, other: object) -> bool: + if isinstance(other, Instant): + return super().__lt__(other) + return NotImplemented + + def __le__(self, other: object) -> bool: + if isinstance(other, Instant): + return super().__le__(other) + return NotImplemented + @property def date(self) -> Date: instant_date = config.date_by_instant_cache.get(self) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index e72a983e38..5d775fcf44 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -383,7 +383,7 @@ def size_in_weekdays(self) -> int: if self.unit == DateUnit.YEAR: return self.size_in_weeks * 7 - if self.unit in DateUnit.MONTH: + if DateUnit.MONTH in self.unit: last = self.start.offset(self.size, self.unit) if last is None: raise NotImplementedError @@ -730,10 +730,18 @@ def offset(self, offset: str | int, unit: t.DateUnit | None = None) -> t.Period: Period((, Instant((2014, 12, 31)), 1)) """ + + start: None | t.Instant = self[1].offset( + offset, self[0] if unit is None else unit + ) + + if start is None: + raise NotImplementedError + return self.__class__( ( self[0], - self[1].offset(offset, self[0] if unit is None else unit), + start, self[2], ), ) diff --git a/openfisca_core/periods/types.py b/openfisca_core/periods/types.py index f8136f5b6b..26a6e096d0 100644 --- a/openfisca_core/periods/types.py +++ b/openfisca_core/periods/types.py @@ -39,6 +39,12 @@ def day(self) -> int: def date(self) -> Date: ... + def __lt__(self, other: object, /) -> bool: + ... + + def __le__(self, other: object, /) -> bool: + ... + def offset(self, offset: str | int, unit: DateUnit) -> Instant | None: ... diff --git a/openfisca_core/types.py b/openfisca_core/types.py index aae0a0540b..bb8ffb9998 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -84,18 +84,14 @@ class ParameterNodeAtInstant(Protocol): # Periods -class Container(Protocol[T_con]): - def __contains__(self, item: T_con, /) -> bool: - ... - - class Indexable(Protocol[T_cov]): def __getitem__(self, index: int, /) -> T_cov: ... -class DateUnit(Container[str], Protocol): - ... +class DateUnit(Protocol): + def __contains__(self, other: object, /) -> bool: + ... class Instant(Indexable[int], Iterable[int], Sized, Protocol): From 7dc706ffc4da5011326b914bfdfba9dbd6cbcd61 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 13:54:44 +0200 Subject: [PATCH 136/188] chore(periods): add py.typed --- openfisca_core/periods/py.typed | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 openfisca_core/periods/py.typed diff --git a/openfisca_core/periods/py.typed b/openfisca_core/periods/py.typed new file mode 100644 index 0000000000..e69de29bb2 From 74b8d1164370c125bd6f226921227b60a02bdd76 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 18 Sep 2024 00:34:58 +0200 Subject: [PATCH 137/188] chore(version): bump --- CHANGELOG.md | 8 ++++++++ setup.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 002cf14714..fdbd9373ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +### 41.5.7 [#1223](https://github.com/openfisca/openfisca-core/pull/1223) + +#### Technical changes + +- Update `pendulum' +- Remove run-time type-checks +- Add typing to the periods module + ### 41.5.6 [#1185](https://github.com/openfisca/openfisca-core/pull/1185) #### Technical changes diff --git a/setup.py b/setup.py index 6b573a28b1..d1661bbf94 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="OpenFisca-Core", - version="41.5.6", + version="41.5.7", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 4f81fb73c5e3c88ccada34f0542ff2f69371d357 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 20 Sep 2024 17:42:37 +0200 Subject: [PATCH 138/188] refactor(periods): fix linter warnings --- openfisca_core/periods/__init__.py | 2 ++ openfisca_core/periods/_errors.py | 3 +++ openfisca_core/periods/_parsers.py | 13 ++++++---- openfisca_core/periods/config.py | 7 ++++-- openfisca_core/periods/date_unit.py | 3 +++ openfisca_core/periods/helpers.py | 25 +++++++++++++++---- openfisca_core/periods/instant_.py | 8 +++--- openfisca_core/periods/period_.py | 6 +++-- openfisca_core/periods/tests/test__parsers.py | 3 +-- openfisca_core/periods/types.py | 7 ++++-- openfisca_core/types.py | 3 +++ 11 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 openfisca_core/periods/_errors.py diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 1f2e68acaf..60300f09c8 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -22,6 +22,7 @@ # See: https://www.python.org/dev/peps/pep-0008/#imports from . import types +from ._errors import ParserError from .config import ( INSTANT_PATTERN, date_by_instant_cache, @@ -71,5 +72,6 @@ "ETERNITY", "ISOFORMAT", "ISOCALENDAR", + "ParserError", "types", ] diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py new file mode 100644 index 0000000000..4ab3c64597 --- /dev/null +++ b/openfisca_core/periods/_errors.py @@ -0,0 +1,3 @@ +from pendulum.parsing.exceptions import ParserError + +__all__ = ["ParserError"] diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index d1b93d4fb2..197a189b58 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -3,10 +3,9 @@ import re import pendulum -from pendulum.datetime import Date -from pendulum.parsing import ParserError from . import types as t +from ._errors import ParserError from .date_unit import DateUnit from .instant_ import Instant from .period_ import Period @@ -35,7 +34,7 @@ def _parse_period(value: str) -> Optional[t.Period]: return None # Check for a non-empty string. - if not (value and isinstance(value, str)): + if len(value) == 0: raise AttributeError # If it's negative, next! @@ -49,7 +48,7 @@ def _parse_period(value: str) -> Optional[t.Period]: unit = _parse_unit(value) date = pendulum.parse(value, exact=True) - if not isinstance(date, Date): + if not isinstance(date, pendulum.Date): raise ValueError instant = Instant((date.year, date.month, date.day)) @@ -95,4 +94,8 @@ def _parse_unit(value: str) -> t.DateUnit: return DateUnit.DAY - raise ValueError + else: + raise ValueError + + +__all__ = ["_parse_period", "_parse_unit"] diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/config.py index 9d3b3c008e..4486a5caf0 100644 --- a/openfisca_core/periods/config.py +++ b/openfisca_core/periods/config.py @@ -1,6 +1,6 @@ import re -from pendulum.datetime import Date +import pendulum from . import types as t @@ -10,8 +10,11 @@ r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$", ) -date_by_instant_cache: dict[t.Instant, Date] = {} +date_by_instant_cache: dict[t.Instant, pendulum.Date] = {} str_by_instant_cache: dict[t.Instant, t.InstantStr] = {} year_or_month_or_day_re = re.compile( r"(18|19|20)\d{2}(-(0?[1-9]|1[0-2])(-([0-2]?\d|3[0-1]))?)?$", ) + + +__all__ = ["INSTANT_PATTERN", "date_by_instant_cache", "str_by_instant_cache"] diff --git a/openfisca_core/periods/date_unit.py b/openfisca_core/periods/date_unit.py index 468b4b0b4b..c91d8ba865 100644 --- a/openfisca_core/periods/date_unit.py +++ b/openfisca_core/periods/date_unit.py @@ -107,3 +107,6 @@ def __contains__(self, other: object) -> bool: MONTH = "month" YEAR = "year" ETERNITY = "eternity" + + +__all__ = ["DateUnit"] diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index e26be20281..d3024d45fc 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -6,10 +6,10 @@ import os import pendulum -from pendulum.parsing import ParserError from . import _parsers, config from . import types as t +from ._errors import ParserError from .date_unit import DateUnit from .instant_ import Instant from .period_ import Period @@ -21,11 +21,16 @@ def instant(instant: None) -> None: @overload -def instant(instant: object) -> Instant: +def instant(instant: int | t.Period | t.Instant | datetime.date) -> t.Instant: ... -def instant(instant: object | None) -> t.Instant | None: +@overload +def instant(instant: str | list[int] | tuple[int, ...]) -> NoReturn | t.Instant: + ... + + +def instant(instant: object) -> None | t.Instant: """Build a new instant, aka a triple of integers (year, month, day). Args: @@ -200,7 +205,7 @@ def period(value: object) -> t.Period: try: period = _parsers._parse_period(value) - except (AttributeError, ParserError, ValueError): + except (AttributeError, ValueError, ParserError): _raise_error(value) if period is not None: @@ -225,7 +230,7 @@ def period(value: object) -> t.Period: try: base_period = _parsers._parse_period(components[1]) - except (AttributeError, ParserError, ValueError): + except (AttributeError, ValueError, ParserError): _raise_error(value) if not base_period: @@ -329,3 +334,13 @@ def unit_weight(unit: t.DateUnit) -> int: """ return unit_weights()[unit] + + +__all__ = [ + "instant", + "instant_date", + "key_period_size", + "period", + "unit_weight", + "unit_weights", +] diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index fd866bc022..22559192a8 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,7 +1,6 @@ from __future__ import annotations import pendulum -from pendulum.datetime import Date from . import config from . import types as t @@ -101,7 +100,7 @@ def __le__(self, other: object) -> bool: return NotImplemented @property - def date(self) -> Date: + def date(self) -> pendulum.Date: instant_date = config.date_by_instant_cache.get(self) if instant_date is None: @@ -151,7 +150,7 @@ def offset(self, offset: str | int, unit: t.DateUnit) -> t.Instant | None: Instant((2019, 12, 29)) """ - year, month, day = self + year, month, _ = self assert unit in ( DateUnit.isoformat + DateUnit.isocalendar @@ -215,3 +214,6 @@ def offset(self, offset: str | int, unit: t.DateUnit) -> t.Instant | None: def eternity(cls) -> t.Instant: """Return an eternity instant.""" return cls((1, 1, 1)) + + +__all__ = ["Instant"] diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 5d775fcf44..6293f173e0 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -225,7 +225,7 @@ def size(self) -> int: return self[2] @property - def date(self) -> Date: + def date(self) -> pendulum.Date: """The date representation of the ``Period`` start date. Examples: @@ -788,7 +788,6 @@ def stop(self) -> t.Instant: """ unit, start_instant, size = self - year, month, day = start_instant if unit == DateUnit.ETERNITY: return Instant.eternity() @@ -893,3 +892,6 @@ def first_weekday(self) -> t.Period: def eternity(cls) -> t.Period: """Return an eternity period.""" return cls((DateUnit.ETERNITY, Instant.eternity(), 0)) + + +__all__ = ["Period"] diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test__parsers.py index 67a2891a32..3f47f5abc5 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test__parsers.py @@ -1,7 +1,6 @@ import pytest -from pendulum.parsing import ParserError -from openfisca_core.periods import DateUnit, Instant, Period, _parsers +from openfisca_core.periods import DateUnit, Instant, ParserError, Period, _parsers @pytest.mark.parametrize( diff --git a/openfisca_core/periods/types.py b/openfisca_core/periods/types.py index 26a6e096d0..5c43337c13 100644 --- a/openfisca_core/periods/types.py +++ b/openfisca_core/periods/types.py @@ -2,7 +2,7 @@ from typing import NewType, Protocol -from pendulum.datetime import Date +import pendulum from openfisca_core import types as t @@ -36,7 +36,7 @@ def day(self) -> int: ... @property - def date(self) -> Date: + def date(self) -> pendulum.Date: ... def __lt__(self, other: object, /) -> bool: @@ -64,3 +64,6 @@ def stop(self) -> Instant: def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: ... + + +__all__ = ["DateUnit", "Instant", "Period", "InstantStr", "PeriodStr"] diff --git a/openfisca_core/types.py b/openfisca_core/types.py index bb8ffb9998..0f6f21892a 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -93,6 +93,9 @@ class DateUnit(Protocol): def __contains__(self, other: object, /) -> bool: ... + def upper(self) -> str: + ... + class Instant(Indexable[int], Iterable[int], Sized, Protocol): ... From b27db7c0a786a30a0fb65fea6f2267b836bfcb9d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 24 Sep 2024 20:17:56 +0200 Subject: [PATCH 139/188] chore: cleanup --- openfisca_core/periods/date_unit.py | 2 +- openfisca_core/periods/helpers.py | 16 ++-- openfisca_core/periods/instant_.py | 3 +- openfisca_core/periods/period_.py | 9 +- .../periods/tests/helpers/test_period.py | 14 +-- openfisca_core/periods/types.py | 68 +------------- openfisca_core/types.py | 93 ++++++++++--------- openfisca_core/variables/variable.py | 9 +- setup.cfg | 2 +- 9 files changed, 72 insertions(+), 144 deletions(-) diff --git a/openfisca_core/periods/date_unit.py b/openfisca_core/periods/date_unit.py index c91d8ba865..9d981f6462 100644 --- a/openfisca_core/periods/date_unit.py +++ b/openfisca_core/periods/date_unit.py @@ -66,7 +66,7 @@ class DateUnit(StrEnum, metaclass=DateUnitMeta): {: 'day'} >>> list(DateUnit) - [, , , ...] + [, , >> len(DateUnit) 6 diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index d3024d45fc..62f51570d7 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -1,14 +1,13 @@ from __future__ import annotations -from typing import NoReturn, Optional, overload +from typing import NoReturn, overload import datetime import os import pendulum -from . import _parsers, config -from . import types as t +from . import _parsers, config, types as t from ._errors import ParserError from .date_unit import DateUnit from .instant_ import Instant @@ -16,18 +15,15 @@ @overload -def instant(instant: None) -> None: - ... +def instant(instant: None) -> None: ... @overload -def instant(instant: int | t.Period | t.Instant | datetime.date) -> t.Instant: - ... +def instant(instant: int | t.Period | t.Instant | datetime.date) -> t.Instant: ... @overload -def instant(instant: str | list[int] | tuple[int, ...]) -> NoReturn | t.Instant: - ... +def instant(instant: str | list[int] | tuple[int, ...]) -> t.Instant: ... def instant(instant: object) -> None | t.Instant: @@ -100,7 +96,7 @@ def instant(instant: object) -> None | t.Instant: return Instant(result) -def instant_date(instant: Optional[t.Instant]) -> Optional[datetime.date]: +def instant_date(instant: None | t.Instant) -> None | datetime.date: """Returns the date representation of an :class:`.Instant`. Args: diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 22559192a8..752a947bd3 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -2,8 +2,7 @@ import pendulum -from . import config -from . import types as t +from . import config, types as t from .date_unit import DateUnit diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 6293f173e0..a55d6635b3 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -7,8 +7,7 @@ import pendulum -from . import helpers -from . import types as t +from . import helpers, types as t from .date_unit import DateUnit from .instant_ import Instant @@ -138,8 +137,8 @@ def __str__(self) -> t.PeriodStr: if month == 1: # civil year starting from january return t.PeriodStr(str(f_year)) - # rolling year - return t.PeriodStr(f"{DateUnit.YEAR}:{f_year}-{month:02d}") + # rolling year + return t.PeriodStr(f"{DateUnit.YEAR}:{f_year}-{month:02d}") # simple month if unit == DateUnit.MONTH and size == 1: @@ -152,7 +151,7 @@ def __str__(self) -> t.PeriodStr: if unit == DateUnit.DAY: if size == 1: return t.PeriodStr(f"{f_year}-{month:02d}-{day:02d}") - return t.PeriodStr(f"{unit}:{f_year}-{month:02d}-{day:02d}:{size}") + return t.PeriodStr(f"{unit}:{f_year}-{month:02d}-{day:02d}:{size}") # 1 week if unit == DateUnit.WEEK and size == 1: diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py index 2db3865b31..6973f89746 100644 --- a/openfisca_core/periods/tests/helpers/test_period.py +++ b/openfisca_core/periods/tests/helpers/test_period.py @@ -9,15 +9,15 @@ @pytest.mark.parametrize( ("arg", "expected"), [ - ["eternity", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0))], - ["ETERNITY", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0))], - [ + ("eternity", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0))), + ("ETERNITY", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0))), + ( DateUnit.ETERNITY, Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0)), - ], - [datetime.date(1, 1, 1), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))], - [Instant((1, 1, 1)), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))], - [ + ), + (datetime.date(1, 1, 1), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))), + (Instant((1, 1, 1)), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))), + ( Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), ), diff --git a/openfisca_core/periods/types.py b/openfisca_core/periods/types.py index 5c43337c13..d29092830d 100644 --- a/openfisca_core/periods/types.py +++ b/openfisca_core/periods/types.py @@ -1,69 +1,3 @@ -from __future__ import annotations - -from typing import NewType, Protocol - -import pendulum - -from openfisca_core import types as t - -# New types. - -#: For example "2000-01". -InstantStr = NewType("InstantStr", str) - -#: For example "1:2000-01-01:day". -PeriodStr = NewType("PeriodStr", str) - - -# Periods - - -class DateUnit(t.DateUnit, Protocol): - ... - - -class Instant(t.Instant, Protocol): - @property - def year(self) -> int: - ... - - @property - def month(self) -> int: - ... - - @property - def day(self) -> int: - ... - - @property - def date(self) -> pendulum.Date: - ... - - def __lt__(self, other: object, /) -> bool: - ... - - def __le__(self, other: object, /) -> bool: - ... - - def offset(self, offset: str | int, unit: DateUnit) -> Instant | None: - ... - - -class Period(t.Period, Protocol): - @property - def size(self) -> int: - ... - - @property - def start(self) -> Instant: - ... - - @property - def stop(self) -> Instant: - ... - - def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: - ... - +from openfisca_core.types import DateUnit, Instant, InstantStr, Period, PeriodStr __all__ = ["DateUnit", "Instant", "Period", "InstantStr", "PeriodStr"] diff --git a/openfisca_core/types.py b/openfisca_core/types.py index 0f6f21892a..d689211a4c 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -3,29 +3,30 @@ from collections.abc import Iterable, Sequence, Sized from numpy.typing import NDArray from typing import Any, TypeVar, Union -from typing_extensions import Protocol, TypeAlias +from typing_extensions import NewType, Protocol, TypeAlias import numpy +import pendulum -N = TypeVar("N", bound=numpy.generic, covariant=True) +_N = TypeVar("_N", bound=numpy.generic, covariant=True) #: Type representing an numpy array. -Array: TypeAlias = NDArray[N] +Array: TypeAlias = NDArray[_N] -L = TypeVar("L") +_L = TypeVar("_L") #: Type representing an array-like object. -ArrayLike: TypeAlias = Sequence[L] +ArrayLike: TypeAlias = Sequence[_L] #: Type variable representing an error. -E = TypeVar("E", covariant=True) +_E = TypeVar("_E", covariant=True) #: Type variable representing a value. -A = TypeVar("A", covariant=True) +_A = TypeVar("_A", covariant=True) #: Generic type vars. -T_cov = TypeVar("T_cov", covariant=True) -T_con = TypeVar("T_con", contravariant=True) +_T_cov = TypeVar("_T_cov", covariant=True) +_T_con = TypeVar("_T_con", contravariant=True) # Entities @@ -35,12 +36,8 @@ class CoreEntity(Protocol): key: Any plural: Any - @abc.abstractmethod def check_role_validity(self, role: Any) -> None: ... - - @abc.abstractmethod def check_variable_defined_for_entity(self, variable_name: Any) -> None: ... - def get_variable( self, variable_name: Any, @@ -67,44 +64,58 @@ def key(self) -> str: ... class Holder(Protocol): - @abc.abstractmethod def clone(self, population: Any) -> Holder: ... - - @abc.abstractmethod def get_memory_usage(self) -> Any: ... # Parameters -class ParameterNodeAtInstant(Protocol): - ... +class ParameterNodeAtInstant(Protocol): ... # Periods +#: For example "2000-01". +InstantStr = NewType("InstantStr", str) -class Indexable(Protocol[T_cov]): - def __getitem__(self, index: int, /) -> T_cov: - ... +#: For example "1:2000-01-01:day". +PeriodStr = NewType("PeriodStr", str) -class DateUnit(Protocol): - def __contains__(self, other: object, /) -> bool: - ... +class Indexable(Protocol[_T_cov]): + def __getitem__(self, index: int, /) -> _T_cov: ... - def upper(self) -> str: - ... + +class DateUnit(Protocol): + def __contains__(self, other: object, /) -> bool: ... + def upper(self) -> str: ... class Instant(Indexable[int], Iterable[int], Sized, Protocol): - ... + @property + def year(self, /) -> int: ... + @property + def month(self, /) -> int: ... + @property + def day(self, /) -> int: ... + @property + def date(self, /) -> pendulum.Date: ... + def __lt__(self, other: object, /) -> bool: ... + def __le__(self, other: object, /) -> bool: ... + def offset(self, offset: str | int, unit: DateUnit, /) -> None | Instant: ... class Period(Indexable[Union[DateUnit, Instant, int]], Protocol): @property - def unit(self) -> DateUnit: - ... + def unit(self) -> DateUnit: ... + @property + def start(self) -> Instant: ... + @property + def size(self) -> int: ... + @property + def stop(self) -> Instant: ... + def offset(self, offset: str | int, unit: None | DateUnit = None) -> Period: ... # Populations @@ -113,25 +124,17 @@ def unit(self) -> DateUnit: class Population(Protocol): entity: Any - def get_holder(self, variable_name: Any) -> Any: - ... + def get_holder(self, variable_name: Any) -> Any: ... # Simulations class Simulation(Protocol): - def calculate(self, variable_name: Any, period: Any) -> Any: - ... - - def calculate_add(self, variable_name: Any, period: Any) -> Any: - ... - - def calculate_divide(self, variable_name: Any, period: Any) -> Any: - ... - - def get_population(self, plural: Any | None) -> Any: - ... + def calculate(self, variable_name: Any, period: Any) -> Any: ... + def calculate_add(self, variable_name: Any, period: Any) -> Any: ... + def calculate_divide(self, variable_name: Any, period: Any) -> Any: ... + def get_population(self, plural: Any | None) -> Any: ... # Tax-Benefit systems @@ -144,8 +147,7 @@ def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Any | None: - """Abstract method.""" + ) -> Any | None: ... # Variables @@ -165,5 +167,4 @@ def __call__( class Params(Protocol): - def __call__(self, instant: Instant) -> ParameterNodeAtInstant: - ... + def __call__(self, instant: Instant) -> ParameterNodeAtInstant: ... diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index d69fed5466..9d7819049b 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -9,8 +9,7 @@ import numpy import sortedcontainers -from openfisca_core import periods, tools -from openfisca_core import types as t +from openfisca_core import periods, tools, types as t from openfisca_core.entities import Entity, GroupEntity from openfisca_core.indexed_enums import Enum, EnumArray from openfisca_core.periods import DateUnit, Period @@ -384,8 +383,8 @@ def get_introspection_data(cls): def get_formula( self, - period: Union[t.Instant, t.Period, str, int] = None, - ) -> Optional[t.Formula]: + period: None | t.Instant | t.Period | str | int = None, + ) -> None | t.Formula: """Returns the formula to compute the variable at the given period. If no period is given and the variable has several formulas, the method @@ -398,7 +397,7 @@ def get_formula( Formula used to compute the variable. """ - instant: Optional[t.Instant] + instant: None | t.Instant if not self.formulas: return None diff --git a/setup.cfg b/setup.cfg index 596ce99153..2d13ef286c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ in-place = true include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors max-line-length = 88 per-file-ignores = - */types.py:D101,D102,E704 + */types.py:D101,D102,E301,E704 */test_*.py:D101,D102,D103 */__init__.py:F401 */__init__.pyi:E302,E704 From a183a3a55f9fc528d8c443c6cf7c277e247d1020 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 17 Sep 2024 13:37:31 +0200 Subject: [PATCH 140/188] chore(deps): update pendulum --- openfisca_core/periods/period_.py | 8 ++++---- openfisca_tasks/lint.mk | 1 + setup.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 0dcf960bbf..14726d5360 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -348,14 +348,14 @@ def size_in_weeks(self): if self.unit == DateUnit.YEAR: start = self.start.date cease = start.add(years=self.size) - delta = pendulum.period(start, cease) - return delta.as_interval().weeks + delta = pendulum.interval(start, cease) + return delta.in_weeks() if self.unit == DateUnit.MONTH: start = self.start.date cease = start.add(months=self.size) - delta = pendulum.period(start, cease) - return delta.as_interval().weeks + delta = pendulum.interval(start, cease) + return delta.in_weeks() if self.unit == DateUnit.WEEK: return self.size diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 445abba10b..7f90aa5d58 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -42,6 +42,7 @@ check-types: @mypy \ openfisca_core/commons \ openfisca_core/entities \ + openfisca_core/periods \ openfisca_core/types.py @$(call print_pass,$@:) diff --git a/setup.py b/setup.py index 2f1c7089bf..6b573a28b1 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ "dpath >=2.1.4, <3.0", "numexpr >=2.8.4, <3.0", "numpy >=1.24.2, <1.25", - "pendulum >=2.1.2, <3.0.0", + "pendulum >=3.0.0, <4.0.0", "psutil >=5.9.4, <6.0", "pytest >=8.3.3, <9.0", "sortedcontainers >=2.4.0, <3.0", From 99ba51a6552eaf870c7df1dd54f6c2cdf03c3ae9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 18 Sep 2024 00:30:21 +0200 Subject: [PATCH 141/188] refactor(types): do not use abstract classes --- openfisca_core/types.py | 55 +++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/openfisca_core/types.py b/openfisca_core/types.py index 16a1f0e90e..5134c518d6 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -1,13 +1,10 @@ from __future__ import annotations -import typing_extensions -from collections.abc import Sequence +from collections.abc import Iterable, Sequence, Sized from numpy.typing import NDArray -from typing import Any, TypeVar +from typing import Any, TypeVar, Union from typing_extensions import Protocol, TypeAlias -import abc - import numpy N = TypeVar("N", bound=numpy.generic, covariant=True) @@ -26,6 +23,10 @@ #: Type variable representing a value. A = TypeVar("A", covariant=True) +#: Generic type vars. +T_cov = TypeVar("T_cov", covariant=True) +T_con = TypeVar("T_con", contravariant=True) + # Entities @@ -34,13 +35,8 @@ class CoreEntity(Protocol): key: Any plural: Any - @abc.abstractmethod def check_role_validity(self, role: Any) -> None: ... - - @abc.abstractmethod def check_variable_defined_for_entity(self, variable_name: Any) -> None: ... - - @abc.abstractmethod def get_variable( self, variable_name: Any, @@ -67,35 +63,36 @@ def key(self) -> str: ... class Holder(Protocol): - @abc.abstractmethod def clone(self, population: Any) -> Holder: ... - - @abc.abstractmethod def get_memory_usage(self) -> Any: ... # Parameters -@typing_extensions.runtime_checkable class ParameterNodeAtInstant(Protocol): ... # Periods -class Instant(Protocol): ... +class Container(Protocol[T_con]): + def __contains__(self, item: T_con, /) -> bool: ... -@typing_extensions.runtime_checkable -class Period(Protocol): - @property - @abc.abstractmethod - def start(self) -> Any: ... +class Indexable(Protocol[T_cov]): + def __getitem__(self, index: int, /) -> T_cov: ... + + +class DateUnit(Container[str], Protocol): ... + +class Instant(Indexable[int], Iterable[int], Sized, Protocol): ... + + +class Period(Indexable[Union[DateUnit, Instant, int]], Protocol): @property - @abc.abstractmethod - def unit(self) -> Any: ... + def unit(self) -> DateUnit: ... # Populations @@ -104,7 +101,6 @@ def unit(self) -> Any: ... class Population(Protocol): entity: Any - @abc.abstractmethod def get_holder(self, variable_name: Any) -> Any: ... @@ -112,16 +108,9 @@ def get_holder(self, variable_name: Any) -> Any: ... class Simulation(Protocol): - @abc.abstractmethod def calculate(self, variable_name: Any, period: Any) -> Any: ... - - @abc.abstractmethod def calculate_add(self, variable_name: Any, period: Any) -> Any: ... - - @abc.abstractmethod def calculate_divide(self, variable_name: Any, period: Any) -> Any: ... - - @abc.abstractmethod def get_population(self, plural: Any | None) -> Any: ... @@ -131,13 +120,11 @@ def get_population(self, plural: Any | None) -> Any: ... class TaxBenefitSystem(Protocol): person_entity: Any - @abc.abstractmethod def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Any | None: - """Abstract method.""" + ) -> Any | None: ... # Variables @@ -148,7 +135,6 @@ class Variable(Protocol): class Formula(Protocol): - @abc.abstractmethod def __call__( self, population: Population, @@ -158,5 +144,4 @@ def __call__( class Params(Protocol): - @abc.abstractmethod def __call__(self, instant: Instant) -> ParameterNodeAtInstant: ... From 9a43147aa1a9cc1ee68db8092c0c7177dd770fec Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 11:43:34 +0200 Subject: [PATCH 142/188] refactor(commons): reuse array type defs --- openfisca_core/commons/__init__.py | 24 ++++++++++++---------- openfisca_core/commons/dummy.py | 3 +++ openfisca_core/commons/formulas.py | 25 +++++++++++++--------- openfisca_core/commons/misc.py | 9 ++++++-- openfisca_core/commons/rates.py | 33 ++++++++++++++++-------------- openfisca_core/commons/types.py | 3 +++ 6 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 openfisca_core/commons/types.py diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index 807abec778..37204d859c 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -50,18 +50,20 @@ """ -# Official Public API - +from . import types +from .dummy import Dummy from .formulas import apply_thresholds, concat, switch from .misc import empty_clone, stringify_array from .rates import average_rate, marginal_rate -__all__ = ["apply_thresholds", "concat", "switch"] -__all__ = ["empty_clone", "stringify_array", *__all__] -__all__ = ["average_rate", "marginal_rate", *__all__] - -# Deprecated - -from .dummy import Dummy - -__all__ = ["Dummy", *__all__] +__all__ = [ + "Dummy", + "apply_thresholds", + "average_rate", + "concat", + "empty_clone", + "marginal_rate", + "stringify_array", + "switch", + "types", +] diff --git a/openfisca_core/commons/dummy.py b/openfisca_core/commons/dummy.py index 5135a8f555..b9fc31d89f 100644 --- a/openfisca_core/commons/dummy.py +++ b/openfisca_core/commons/dummy.py @@ -20,3 +20,6 @@ def __init__(self) -> None: "and will be removed in the future.", ] warnings.warn(" ".join(message), DeprecationWarning, stacklevel=2) + + +__all__ = ["Dummy"] diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 909c4cd14a..eb49a9f527 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,16 +1,17 @@ +from __future__ import annotations + from collections.abc import Mapping -from typing import Union import numpy -from openfisca_core import types as t +from . import types as t def apply_thresholds( - input: t.Array[numpy.float64], + input: t.Array[numpy.float32], thresholds: t.ArrayLike[float], choices: t.ArrayLike[float], -) -> t.Array[numpy.float64]: +) -> t.Array[numpy.float32]: """Makes a choice based on an input and thresholds. From a list of ``choices``, this function selects one of these values @@ -38,7 +39,8 @@ def apply_thresholds( array([10, 10, 15, 15, 20]) """ - condlist: list[Union[t.Array[numpy.bool_], bool]] + + condlist: list[t.Array[numpy.bool_] | bool] condlist = [input <= threshold for threshold in thresholds] if len(condlist) == len(choices) - 1: @@ -54,8 +56,8 @@ def apply_thresholds( def concat( - this: Union[t.Array[numpy.str_], t.ArrayLike[str]], - that: Union[t.Array[numpy.str_], t.ArrayLike[str]], + this: t.Array[numpy.str_] | t.ArrayLike[str], + that: t.Array[numpy.str_] | t.ArrayLike[str], ) -> t.Array[numpy.str_]: """Concatenates the values of two arrays. @@ -84,10 +86,10 @@ def concat( def switch( - conditions: t.Array[numpy.float64], + conditions: t.Array[numpy.float32], value_by_condition: Mapping[float, float], -) -> t.Array[numpy.float64]: - """Mimicks a switch statement. +) -> t.Array[numpy.float32]: + """Mimick a switch statement. Given an array of conditions, returns an array of the same size, replacing each condition item with the matching given value. @@ -117,3 +119,6 @@ def switch( condlist = [conditions == condition for condition in value_by_condition] return numpy.select(condlist, tuple(value_by_condition.values())) + + +__all__ = ["apply_thresholds", "concat", "switch"] diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 3c9cd5feab..7e4fd50932 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,4 +1,6 @@ -from typing import Optional, TypeVar +from __future__ import annotations + +from typing import TypeVar import numpy @@ -44,7 +46,7 @@ def empty_clone(original: T) -> T: return new -def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str: +def stringify_array(array: None | t.Array[numpy.generic]) -> str: """Generates a clean string representation of a numpy array. Args: @@ -76,3 +78,6 @@ def stringify_array(array: Optional[t.Array[numpy.generic]]) -> str: return "None" return f"[{', '.join(str(cell) for cell in array)}]" + + +__all__ = ["empty_clone", "stringify_array"] diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index e5c3114a71..3a1ec661c2 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -1,16 +1,16 @@ -from typing import Optional - -from openfisca_core.types import Array, ArrayLike +from __future__ import annotations import numpy +from . import types as t + def average_rate( - target: Array[numpy.float64], - varying: ArrayLike[float], - trim: Optional[ArrayLike[float]] = None, -) -> Array[numpy.float64]: - """Computes the average rate of a target net income. + target: t.Array[numpy.float32], + varying: t.ArrayLike[float], + trim: None | t.ArrayLike[float] = None, +) -> t.Array[numpy.float32]: + """Compute 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 of the lower and @@ -40,8 +40,8 @@ def average_rate( array([ nan, 0. , -0.5]) """ - average_rate: Array[numpy.float64] + average_rate: t.Array[numpy.float32] average_rate = 1 - target / varying if trim is not None: @@ -61,11 +61,11 @@ def average_rate( def marginal_rate( - target: Array[numpy.float64], - varying: Array[numpy.float64], - trim: Optional[ArrayLike[float]] = None, -) -> Array[numpy.float64]: - """Computes the marginal rate of a target net income. + target: t.Array[numpy.float32], + varying: t.Array[numpy.float32], + trim: None | t.ArrayLike[float] = None, +) -> t.Array[numpy.float32]: + """Compute 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 @@ -95,8 +95,8 @@ def marginal_rate( array([nan, 0.5]) """ - marginal_rate: Array[numpy.float64] + marginal_rate: t.Array[numpy.float32] marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) if trim is not None: @@ -113,3 +113,6 @@ def marginal_rate( ) return marginal_rate + + +__all__ = ["average_rate", "marginal_rate"] diff --git a/openfisca_core/commons/types.py b/openfisca_core/commons/types.py new file mode 100644 index 0000000000..39c067f455 --- /dev/null +++ b/openfisca_core/commons/types.py @@ -0,0 +1,3 @@ +from openfisca_core.types import Array, ArrayLike + +__all__ = ["Array", "ArrayLike"] From 66339b83c97227b5654f1417ce1f4c88653bb1ea Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 18:23:56 +0200 Subject: [PATCH 143/188] refactor(tools): move eval_expression to commons --- openfisca_core/commons/__init__.py | 4 ++- openfisca_core/commons/misc.py | 29 ++++++++++++++++++- openfisca_core/indexed_enums/__init__.py | 14 +++++++-- openfisca_core/indexed_enums/enum.py | 2 +- openfisca_core/indexed_enums/enum_array.py | 6 ++-- openfisca_core/indexed_enums/types.py | 3 ++ openfisca_core/parameters/parameter_node.py | 6 ++-- openfisca_core/periods/period_.py | 8 ++--- openfisca_core/populations/population.py | 2 +- .../simulations/simulation_builder.py | 4 +-- .../taxscales/abstract_rate_tax_scale.py | 2 +- .../taxscales/abstract_tax_scale.py | 4 +-- .../linear_average_rate_tax_scale.py | 4 +-- .../taxscales/marginal_amount_tax_scale.py | 4 +-- .../taxscales/marginal_rate_tax_scale.py | 12 ++++---- .../taxscales/rate_tax_scale_like.py | 4 +-- .../taxscales/single_amount_tax_scale.py | 4 +-- openfisca_core/taxscales/tax_scale_like.py | 4 +-- openfisca_core/tools/__init__.py | 12 ++------ openfisca_core/tools/simulation_dumper.py | 4 +-- openfisca_core/variables/variable.py | 4 +-- setup.cfg | 2 +- stubs/numexpr/__init__.pyi | 9 ++++++ 23 files changed, 94 insertions(+), 53 deletions(-) create mode 100644 openfisca_core/indexed_enums/types.py create mode 100644 stubs/numexpr/__init__.pyi diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index 37204d859c..039bd0673f 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -8,6 +8,7 @@ * :func:`.average_rate` * :func:`.concat` * :func:`.empty_clone` + * :func:`.eval_expression` * :func:`.marginal_rate` * :func:`.stringify_array` * :func:`.switch` @@ -53,7 +54,7 @@ from . import types from .dummy import Dummy from .formulas import apply_thresholds, concat, switch -from .misc import empty_clone, stringify_array +from .misc import empty_clone, eval_expression, stringify_array from .rates import average_rate, marginal_rate __all__ = [ @@ -62,6 +63,7 @@ "average_rate", "concat", "empty_clone", + "eval_expression", "marginal_rate", "stringify_array", "switch", diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 7e4fd50932..99b830099e 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -2,6 +2,7 @@ from typing import TypeVar +import numexpr import numpy from openfisca_core import types as t @@ -80,4 +81,30 @@ def stringify_array(array: None | t.Array[numpy.generic]) -> str: return f"[{', '.join(str(cell) for cell in array)}]" -__all__ = ["empty_clone", "stringify_array"] +def eval_expression( + expression: str, +) -> str | t.Array[numpy.bool_] | t.Array[numpy.int32] | t.Array[numpy.float32]: + """Evaluate a string expression to a numpy array. + + Args: + expression(str): An expression to evaluate. + + Returns: + :obj:`object`: The result of the evaluation. + + Examples: + >>> eval_expression("1 + 2") + array(3, dtype=int32) + + >>> eval_expression("salary") + 'salary' + + """ + + try: + return numexpr.evaluate(expression) + except (KeyError, TypeError): + return expression + + +__all__ = ["empty_clone", "eval_expression", "stringify_array"] diff --git a/openfisca_core/indexed_enums/__init__.py b/openfisca_core/indexed_enums/__init__.py index 874a7e1f9b..9c4ff7dd6d 100644 --- a/openfisca_core/indexed_enums/__init__.py +++ b/openfisca_core/indexed_enums/__init__.py @@ -21,6 +21,14 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .config import ENUM_ARRAY_DTYPE # noqa: F401 -from .enum import Enum # noqa: F401 -from .enum_array import EnumArray # noqa: F401 +from . import types +from .config import ENUM_ARRAY_DTYPE +from .enum import Enum +from .enum_array import EnumArray + +__all__ = [ + "ENUM_ARRAY_DTYPE", + "Enum", + "EnumArray", + "types", +] diff --git a/openfisca_core/indexed_enums/enum.py b/openfisca_core/indexed_enums/enum.py index 25b02cee74..ec1afa45a9 100644 --- a/openfisca_core/indexed_enums/enum.py +++ b/openfisca_core/indexed_enums/enum.py @@ -30,7 +30,7 @@ def __init__(self, name: str) -> None: @classmethod def encode( cls, - array: EnumArray | numpy.int_ | numpy.float64 | numpy.object_, + array: EnumArray | numpy.int32 | numpy.float32 | numpy.object_, ) -> EnumArray: """Encode a string numpy array, an enum item numpy array, or an int numpy array into an :any:`EnumArray`. See :any:`EnumArray.decode` for diff --git a/openfisca_core/indexed_enums/enum_array.py b/openfisca_core/indexed_enums/enum_array.py index 86b55f9f48..1b6c512b8e 100644 --- a/openfisca_core/indexed_enums/enum_array.py +++ b/openfisca_core/indexed_enums/enum_array.py @@ -5,6 +5,8 @@ import numpy +from . import types as t + if typing.TYPE_CHECKING: from openfisca_core.indexed_enums import Enum @@ -20,7 +22,7 @@ class EnumArray(numpy.ndarray): # https://docs.scipy.org/doc/numpy-1.13.0/user/basics.subclassing.html#slightly-more-realistic-example-attribute-added-to-existing-array. def __new__( cls, - input_array: numpy.int_, + input_array: t.Array[numpy.int16], possible_values: type[Enum] | None = None, ) -> EnumArray: obj = numpy.asarray(input_array).view(cls) @@ -28,7 +30,7 @@ def __new__( return obj # See previous comment - def __array_finalize__(self, obj: numpy.int_ | None) -> None: + def __array_finalize__(self, obj: numpy.int32 | None) -> None: if obj is None: return diff --git a/openfisca_core/indexed_enums/types.py b/openfisca_core/indexed_enums/types.py new file mode 100644 index 0000000000..43c38780ff --- /dev/null +++ b/openfisca_core/indexed_enums/types.py @@ -0,0 +1,3 @@ +from openfisca_core.types import Array + +__all__ = ["Array"] diff --git a/openfisca_core/parameters/parameter_node.py b/openfisca_core/parameters/parameter_node.py index 2be3a9acfd..6f43379b36 100644 --- a/openfisca_core/parameters/parameter_node.py +++ b/openfisca_core/parameters/parameter_node.py @@ -1,6 +1,6 @@ from __future__ import annotations -import typing +from collections.abc import Iterable import copy import os @@ -16,9 +16,7 @@ class ParameterNode(AtInstantLike): """A node in the legislation `parameter tree `_.""" - _allowed_keys: None | (typing.Iterable[str]) = ( - None # By default, no restriction on the keys - ) + _allowed_keys: None | Iterable[str] = None # By default, no restriction on the keys def __init__(self, name="", directory_path=None, data=None, file_path=None) -> None: """Instantiate a ParameterNode either from a dict, (using `data`), or from a directory containing YAML files (using `directory_path`). diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 14726d5360..0dcf960bbf 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -348,14 +348,14 @@ def size_in_weeks(self): if self.unit == DateUnit.YEAR: start = self.start.date cease = start.add(years=self.size) - delta = pendulum.interval(start, cease) - return delta.in_weeks() + delta = pendulum.period(start, cease) + return delta.as_interval().weeks if self.unit == DateUnit.MONTH: start = self.start.date cease = start.add(months=self.size) - delta = pendulum.interval(start, cease) - return delta.in_weeks() + delta = pendulum.period(start, cease) + return delta.as_interval().weeks if self.unit == DateUnit.WEEK: return self.size diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index f9eee1c2a2..06acc05d28 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -84,7 +84,7 @@ def check_period_validity( variable_name: str, period: int | str | Period | None, ) -> None: - if isinstance(period, (int, str, Period)): + if isinstance(period, (int, str, periods.Period)): return stack = traceback.extract_stack() diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 1ebb499239..96d451cd78 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -369,7 +369,7 @@ def join_with_persons( if numpy.issubdtype(roles_array.dtype, numpy.integer): group_population.members_role = numpy.array(flattened_roles)[roles_array] elif len(flattened_roles) == 0: - group_population.members_role = numpy.int64(0) + group_population.members_role = numpy.int16(0) else: group_population.members_role = numpy.select( [roles_array == role.key for role in flattened_roles], @@ -754,7 +754,7 @@ def expand_axes(self) -> None: ) # Adjust ids original_ids: list[str] = self.get_ids(entity_name) * cell_count - indices: Array[numpy.int_] = numpy.arange( + indices: Array[numpy.int16] = numpy.arange( 0, cell_count * self.entity_counts[entity_name], ) diff --git a/openfisca_core/taxscales/abstract_rate_tax_scale.py b/openfisca_core/taxscales/abstract_rate_tax_scale.py index 84ab4eb913..9d828ed673 100644 --- a/openfisca_core/taxscales/abstract_rate_tax_scale.py +++ b/openfisca_core/taxscales/abstract_rate_tax_scale.py @@ -9,7 +9,7 @@ if typing.TYPE_CHECKING: import numpy - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class AbstractRateTaxScale(RateTaxScaleLike): diff --git a/openfisca_core/taxscales/abstract_tax_scale.py b/openfisca_core/taxscales/abstract_tax_scale.py index 933f36d47d..de9a6348c5 100644 --- a/openfisca_core/taxscales/abstract_tax_scale.py +++ b/openfisca_core/taxscales/abstract_tax_scale.py @@ -9,7 +9,7 @@ if typing.TYPE_CHECKING: import numpy - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class AbstractTaxScale(TaxScaleLike): @@ -21,7 +21,7 @@ def __init__( self, name: str | None = None, option: typing.Any = None, - unit: numpy.int_ = None, + unit: numpy.int16 = None, ) -> None: message = [ "The 'AbstractTaxScale' class has been deprecated since", diff --git a/openfisca_core/taxscales/linear_average_rate_tax_scale.py b/openfisca_core/taxscales/linear_average_rate_tax_scale.py index ec1b22e0c2..ffccfc2205 100644 --- a/openfisca_core/taxscales/linear_average_rate_tax_scale.py +++ b/openfisca_core/taxscales/linear_average_rate_tax_scale.py @@ -13,7 +13,7 @@ log = logging.getLogger(__name__) if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class LinearAverageRateTaxScale(RateTaxScaleLike): @@ -21,7 +21,7 @@ def calc( self, tax_base: NumericalArray, right: bool = False, - ) -> numpy.float64: + ) -> numpy.float32: if len(self.rates) == 1: return tax_base * self.rates[0] diff --git a/openfisca_core/taxscales/marginal_amount_tax_scale.py b/openfisca_core/taxscales/marginal_amount_tax_scale.py index ac021351be..aa96bff57b 100644 --- a/openfisca_core/taxscales/marginal_amount_tax_scale.py +++ b/openfisca_core/taxscales/marginal_amount_tax_scale.py @@ -7,7 +7,7 @@ from .amount_tax_scale_like import AmountTaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class MarginalAmountTaxScale(AmountTaxScaleLike): @@ -15,7 +15,7 @@ def calc( self, tax_base: NumericalArray, right: bool = False, - ) -> numpy.float64: + ) -> numpy.float32: """Matches the input amount to a set of brackets and returns the sum of cell values from the lowest bracket to the one containing the input. """ diff --git a/openfisca_core/taxscales/marginal_rate_tax_scale.py b/openfisca_core/taxscales/marginal_rate_tax_scale.py index c81da8e7e9..803a5f8547 100644 --- a/openfisca_core/taxscales/marginal_rate_tax_scale.py +++ b/openfisca_core/taxscales/marginal_rate_tax_scale.py @@ -12,7 +12,7 @@ from .rate_tax_scale_like import RateTaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class MarginalRateTaxScale(RateTaxScaleLike): @@ -37,7 +37,7 @@ def calc( tax_base: NumericalArray, factor: float = 1.0, round_base_decimals: int | None = None, - ) -> numpy.float64: + ) -> numpy.float32: """Compute the tax amount for the given tax bases by applying a taxscale. :param ndarray tax_base: Array of the tax bases. @@ -119,7 +119,7 @@ def marginal_rates( tax_base: NumericalArray, factor: float = 1.0, round_base_decimals: int | None = None, - ) -> numpy.float64: + ) -> numpy.float32: """Compute the marginal tax rates relevant for the given tax bases. :param ndarray tax_base: Array of the tax bases. @@ -149,8 +149,8 @@ def marginal_rates( def rate_from_bracket_indice( self, - bracket_indice: numpy.int_, - ) -> numpy.float64: + bracket_indice: numpy.int16, + ) -> numpy.float32: """Compute the relevant tax rates for the given bracket indices. :param: ndarray bracket_indice: Array of the bracket indices. @@ -186,7 +186,7 @@ def rate_from_bracket_indice( def rate_from_tax_base( self, tax_base: NumericalArray, - ) -> numpy.float64: + ) -> numpy.float32: """Compute the relevant tax rates for the given tax bases. :param: ndarray tax_base: Array of the tax bases. diff --git a/openfisca_core/taxscales/rate_tax_scale_like.py b/openfisca_core/taxscales/rate_tax_scale_like.py index 60ea9c20e1..288226f11e 100644 --- a/openfisca_core/taxscales/rate_tax_scale_like.py +++ b/openfisca_core/taxscales/rate_tax_scale_like.py @@ -14,7 +14,7 @@ from .tax_scale_like import TaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class RateTaxScaleLike(TaxScaleLike, abc.ABC): @@ -128,7 +128,7 @@ def bracket_indices( tax_base: NumericalArray, factor: float = 1.0, round_decimals: int | None = None, - ) -> numpy.int_: + ) -> numpy.int32: """Compute the relevant bracket indices for the given tax bases. :param ndarray tax_base: Array of the tax bases. diff --git a/openfisca_core/taxscales/single_amount_tax_scale.py b/openfisca_core/taxscales/single_amount_tax_scale.py index 1a39396398..1c8cf69a32 100644 --- a/openfisca_core/taxscales/single_amount_tax_scale.py +++ b/openfisca_core/taxscales/single_amount_tax_scale.py @@ -7,7 +7,7 @@ from openfisca_core.taxscales import AmountTaxScaleLike if typing.TYPE_CHECKING: - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class SingleAmountTaxScale(AmountTaxScaleLike): @@ -15,7 +15,7 @@ def calc( self, tax_base: NumericalArray, right: bool = False, - ) -> numpy.float64: + ) -> numpy.float32: """Matches the input amount to a set of brackets and returns the single cell value that fits within that bracket. """ diff --git a/openfisca_core/taxscales/tax_scale_like.py b/openfisca_core/taxscales/tax_scale_like.py index 683c771127..e8680b9f8f 100644 --- a/openfisca_core/taxscales/tax_scale_like.py +++ b/openfisca_core/taxscales/tax_scale_like.py @@ -10,7 +10,7 @@ if typing.TYPE_CHECKING: import numpy - NumericalArray = typing.Union[numpy.int_, numpy.float64] + NumericalArray = typing.Union[numpy.int32, numpy.float32] class TaxScaleLike(abc.ABC): @@ -55,7 +55,7 @@ def calc( self, tax_base: NumericalArray, right: bool, - ) -> numpy.float64: ... + ) -> numpy.float32: ... @abc.abstractmethod def to_dict(self) -> dict: ... diff --git a/openfisca_core/tools/__init__.py b/openfisca_core/tools/__init__.py index 1416ed1529..952dca6ebd 100644 --- a/openfisca_core/tools/__init__.py +++ b/openfisca_core/tools/__init__.py @@ -1,7 +1,6 @@ import os -import numexpr - +from openfisca_core import commons from openfisca_core.indexed_enums import EnumArray @@ -33,7 +32,7 @@ def assert_near( target_value = numpy.array(target_value, dtype=value.dtype) assert_datetime_equals(value, target_value, message) if isinstance(target_value, str): - target_value = eval_expression(target_value) + target_value = commons.eval_expression(target_value) target_value = numpy.array(target_value).astype(numpy.float32) @@ -87,10 +86,3 @@ def get_trace_tool_link(scenario, variables, api_url, trace_tool_url): }, ) ) - - -def eval_expression(expression): - try: - return numexpr.evaluate(expression) - except (KeyError, TypeError): - return expression diff --git a/openfisca_core/tools/simulation_dumper.py b/openfisca_core/tools/simulation_dumper.py index 9b1f5708ad..84898165fd 100644 --- a/openfisca_core/tools/simulation_dumper.py +++ b/openfisca_core/tools/simulation_dumper.py @@ -81,7 +81,7 @@ def _dump_entity(population, directory) -> None: flattened_roles = population.entity.flattened_roles if len(flattened_roles) == 0: - encoded_roles = numpy.int64(0) + encoded_roles = numpy.int16(0) else: encoded_roles = numpy.select( [population.members_role == role for role in flattened_roles], @@ -106,7 +106,7 @@ def _restore_entity(population, directory): flattened_roles = population.entity.flattened_roles if len(flattened_roles) == 0: - population.members_role = numpy.int64(0) + population.members_role = numpy.int16(0) else: population.members_role = numpy.select( [encoded_roles == role.key for role in flattened_roles], diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 77411c32bb..e1aa501080 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -11,7 +11,7 @@ import numpy import sortedcontainers -from openfisca_core import periods, tools +from openfisca_core import commons, periods from openfisca_core.entities import Entity, GroupEntity from openfisca_core.indexed_enums import Enum, EnumArray from openfisca_core.periods import DateUnit, Period @@ -452,7 +452,7 @@ def check_set_value(self, value): ) if self.value_type in (float, int) and isinstance(value, str): try: - value = tools.eval_expression(value) + value = commons.eval_expression(value) except SyntaxError: msg = f"I couldn't understand '{value}' as a value for '{self.name}'" raise ValueError( diff --git a/setup.cfg b/setup.cfg index 596ce99153..2d13ef286c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ in-place = true include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors max-line-length = 88 per-file-ignores = - */types.py:D101,D102,E704 + */types.py:D101,D102,E301,E704 */test_*.py:D101,D102,D103 */__init__.py:F401 */__init__.pyi:E302,E704 diff --git a/stubs/numexpr/__init__.pyi b/stubs/numexpr/__init__.pyi new file mode 100644 index 0000000000..95a0f4f80f --- /dev/null +++ b/stubs/numexpr/__init__.pyi @@ -0,0 +1,9 @@ +from __future__ import annotations + +from numpy.typing import NDArray + +import numpy + +def evaluate( + __ex: str, *__args: object, **__kwargs: object +) -> NDArray[numpy.bool_]| NDArray[numpy.int32] | NDArray[numpy.float32]: ... From 5b9511fa6b25bacadaa7353798c075ce073c09e9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 19:16:03 +0200 Subject: [PATCH 144/188] build(isort): fix imports config --- openfisca_core/commons/misc.py | 2 +- openfisca_core/holders/holder.py | 3 +-- openfisca_core/parameters/config.py | 4 ++-- openfisca_tasks/lint.mk | 4 ++-- setup.cfg | 7 ++++++- stubs/numexpr/__init__.pyi | 2 +- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 99b830099e..1678aace28 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -11,7 +11,7 @@ def empty_clone(original: T) -> T: - """Creates an empty instance of the same class of the original object. + """Create an empty instance of the same class of the original object. Args: original: An object to clone. diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index a8ddf3ed3a..7183d4a44a 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -15,7 +15,6 @@ errors, indexed_enums as enums, periods, - tools, types, ) @@ -235,7 +234,7 @@ def set_input( warning_message = f"You cannot set a value for the variable {self.variable.name}, as it has been neutralized. The value you provided ({array}) will be ignored." return warnings.warn(warning_message, Warning, stacklevel=2) if self.variable.value_type in (float, int) and isinstance(array, str): - array = tools.eval_expression(array) + array = commons.eval_expression(array) if self.variable.set_input: return self.variable.set_input(self, period, array) return self._set(period, array) diff --git a/openfisca_core/parameters/config.py b/openfisca_core/parameters/config.py index b97462a79d..5fb1198bea 100644 --- a/openfisca_core/parameters/config.py +++ b/openfisca_core/parameters/config.py @@ -15,8 +15,8 @@ "so that it is used in your Python environment." + os.linesep, ] warnings.warn(" ".join(message), LibYAMLWarning, stacklevel=2) - from yaml import ( - Loader, # type: ignore # (see https://github.com/python/mypy/issues/1153#issuecomment-455802270) + from yaml import ( # type: ignore # (see https://github.com/python/mypy/issues/1153#issuecomment-455802270) + Loader, ) # 'unit' and 'reference' are only listed here for backward compatibility. diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 7f90aa5d58..8afba8cd97 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -9,7 +9,7 @@ check-syntax-errors: . @$(call print_pass,$@:) ## Run linters to check for syntax and style errors. -check-style: $(shell git ls-files "*.py") +check-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) @isort --check $? @black --check $? @@ -47,7 +47,7 @@ check-types: @$(call print_pass,$@:) ## Run code formatters to correct style errors. -format-style: $(shell git ls-files "*.py") +format-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) @isort $? @black $? diff --git a/setup.cfg b/setup.cfg index 2d13ef286c..7b826f8185 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,12 @@ docstring_style = google extend-ignore = D ignore = B019, E203, E501, F405, E701, E704, RST212, RST301, W503 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors +include-in-doctest = + openfisca_core/commons + openfisca_core/entities + openfisca_core/holders + openfisca_core/periods + openfisca_core/projectors max-line-length = 88 per-file-ignores = */types.py:D101,D102,E301,E704 diff --git a/stubs/numexpr/__init__.pyi b/stubs/numexpr/__init__.pyi index 95a0f4f80f..6607dd9ee9 100644 --- a/stubs/numexpr/__init__.pyi +++ b/stubs/numexpr/__init__.pyi @@ -6,4 +6,4 @@ import numpy def evaluate( __ex: str, *__args: object, **__kwargs: object -) -> NDArray[numpy.bool_]| NDArray[numpy.int32] | NDArray[numpy.float32]: ... +) -> NDArray[numpy.bool_] | NDArray[numpy.int32] | NDArray[numpy.float32]: ... From 176afe37cdbc534e4d1e9015ca0cbdca27292ac7 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 19:27:28 +0200 Subject: [PATCH 145/188] doc(commons): add doctest to eval_expression --- openfisca_core/commons/misc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 1678aace28..e9ca604e8c 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -103,6 +103,7 @@ def eval_expression( try: return numexpr.evaluate(expression) + except (KeyError, TypeError): return expression From 277bd75cd0a31213d55c0bfd0c9dc46e1e2b492a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 20 Sep 2024 19:19:43 +0200 Subject: [PATCH 146/188] refactor(commons): fix linter warnings --- openfisca_core/commons/formulas.py | 2 +- openfisca_core/commons/misc.py | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index eb49a9f527..4781821747 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -59,7 +59,7 @@ def concat( this: t.Array[numpy.str_] | t.ArrayLike[str], that: t.Array[numpy.str_] | t.ArrayLike[str], ) -> t.Array[numpy.str_]: - """Concatenates the values of two arrays. + """Concatenate the values of two arrays. Args: this: An array to concatenate. diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index e9ca604e8c..138dd18e1e 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,16 +1,12 @@ from __future__ import annotations -from typing import TypeVar - import numexpr import numpy from openfisca_core import types as t -T = TypeVar("T") - -def empty_clone(original: T) -> T: +def empty_clone(original: object) -> object: """Create an empty instance of the same class of the original object. Args: @@ -33,13 +29,11 @@ def empty_clone(original: T) -> T: True """ - Dummy: object - new: T Dummy = type( "Dummy", (original.__class__,), - {"__init__": lambda self: None}, + {"__init__": lambda _: None}, ) new = Dummy() @@ -48,7 +42,7 @@ def empty_clone(original: T) -> T: def stringify_array(array: None | t.Array[numpy.generic]) -> str: - """Generates a clean string representation of a numpy array. + """Generate a clean string representation of a numpy array. Args: array: An array. From 52a2584c3209d71498871d9911913eb99d7c330f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 25 Sep 2024 18:05:47 +0200 Subject: [PATCH 147/188] fix(commons): doc line length --- openfisca_core/commons/formulas.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 4781821747..5232c3ba4b 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -48,9 +48,11 @@ def apply_thresholds( # 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." + msg = ( + "'apply_thresholds' must be called with the same number of thresholds " + "than choices, or one more choice." + ) + assert len(condlist) == len(choices), msg return numpy.select(condlist, choices) From da1139e4b61d9a5a4b8a1e0a9cebc8b5a36fcede Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 25 Sep 2024 20:32:53 +0200 Subject: [PATCH 148/188] fix(entities): types --- openfisca_core/entities/_core_entity.py | 49 ++++++++----- openfisca_core/entities/_description.py | 55 +++++++++++++++ openfisca_core/entities/entity.py | 13 +++- openfisca_core/entities/group_entity.py | 16 +++-- openfisca_core/entities/helpers.py | 16 +++-- openfisca_core/entities/role.py | 89 ++++++------------------ openfisca_core/entities/types.py | 69 ++++++++----------- openfisca_core/types.py | 92 +++++++++++++++---------- openfisca_tasks/lint.mk | 1 - setup.cfg | 2 +- setup.py | 4 +- stubs/numexpr/__init__.pyi | 6 +- 12 files changed, 223 insertions(+), 189 deletions(-) create mode 100644 openfisca_core/entities/_description.py diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index da3e6ea981..ca3553379e 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -1,7 +1,9 @@ from __future__ import annotations +from typing import ClassVar + +import abc import os -from abc import abstractmethod from . import types as t from .role import Role @@ -12,29 +14,30 @@ class _CoreEntity: #: A key to identify the entity. key: t.EntityKey + #: The ``key``, pluralised. - plural: t.EntityPlural | None + plural: t.EntityPlural #: A summary description. - label: str | None + label: str #: A full description. - doc: str | None + doc: str #: Whether the entity is a person or not. - is_person: bool + is_person: ClassVar[bool] #: A TaxBenefitSystem instance. - _tax_benefit_system: t.TaxBenefitSystem | None = None + _tax_benefit_system: None | t.TaxBenefitSystem = None - @abstractmethod + @abc.abstractmethod def __init__( self, - key: str, - plural: str, - label: str, - doc: str, - *args: object, + __key: str, + __plural: str, + __label: str, + __doc: str, + *__args: object, ) -> None: ... def __repr__(self) -> str: @@ -46,7 +49,7 @@ def set_tax_benefit_system(self, tax_benefit_system: t.TaxBenefitSystem) -> None def get_variable( self, - variable_name: str, + variable_name: t.VariableName, check_existence: bool = False, ) -> t.Variable | None: """Get a ``variable_name`` from ``variables``.""" @@ -57,16 +60,20 @@ def get_variable( ) return self._tax_benefit_system.get_variable(variable_name, check_existence) - def check_variable_defined_for_entity(self, variable_name: str) -> None: + def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> None: """Check if ``variable_name`` is defined for ``self``.""" - variable: t.Variable | None - entity: t.CoreEntity - - variable = self.get_variable(variable_name, check_existence=True) + entity: None | t.CoreEntity = None + variable: None | t.Variable = self.get_variable( + variable_name, + check_existence=True, + ) if variable is not None: entity = variable.entity + if entity is None: + return + if entity.key != self.key: message = ( f"You tried to compute the variable '{variable_name}' for", @@ -77,8 +84,12 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None: ) raise ValueError(os.linesep.join(message)) - def check_role_validity(self, role: object) -> None: + @staticmethod + def check_role_validity(role: object) -> None: """Check if a ``role`` is an instance of Role.""" if role is not None and not isinstance(role, Role): msg = f"{role} is not a valid role" raise ValueError(msg) + + +__all__ = ["_CoreEntity"] diff --git a/openfisca_core/entities/_description.py b/openfisca_core/entities/_description.py new file mode 100644 index 0000000000..6e0a62beb8 --- /dev/null +++ b/openfisca_core/entities/_description.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +import dataclasses +import textwrap + + +@dataclasses.dataclass(frozen=True) +class _Description: + r"""A Role's description. + + Examples: + >>> data = { + ... "key": "parent", + ... "label": "Parents", + ... "plural": "parents", + ... "doc": "\t\t\tThe one/two adults in charge of the household.", + ... } + + >>> description = _Description(**data) + + >>> repr(_Description) + "" + + >>> repr(description) + "_Description(key='parent', plural='parents', label='Parents', ...)" + + >>> str(description) + "_Description(key='parent', plural='parents', label='Parents', ...)" + + >>> {description} + {_Description(key='parent', plural='parents', label='Parents', doc=...} + + >>> description.key + 'parent' + + """ + + #: A key to identify the Role. + key: str + + #: The ``key``, pluralised. + plural: None | str = None + + #: A summary description. + label: None | str = None + + #: A full description, non-indented. + doc: None | str = None + + def __post_init__(self) -> None: + if self.doc is not None: + object.__setattr__(self, "doc", textwrap.dedent(self.doc)) + + +__all__ = ["_Description"] diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index a3fbaddac3..3b1a6713e8 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,3 +1,5 @@ +from typing import ClassVar + import textwrap from . import types as t @@ -5,11 +7,16 @@ class Entity(_CoreEntity): - """Represents an entity (e.g. a person, a household, etc.) on which calculations can be run.""" + """An entity (e.g. a person, a household) on which calculations can be run.""" + + #: Whether the entity is a person or not. + is_person: ClassVar[bool] = True def __init__(self, key: str, plural: str, label: str, doc: str) -> None: self.key = t.EntityKey(key) - self.label = label self.plural = t.EntityPlural(plural) + self.label = label self.doc = textwrap.dedent(doc) - self.is_person = True + + +__all__ = ["Entity"] diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index b47c92a525..5ae74de560 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,6 +1,7 @@ from __future__ import annotations from collections.abc import Iterable, Sequence +from typing import ClassVar import textwrap from itertools import chain @@ -28,6 +29,9 @@ class GroupEntity(_CoreEntity): """ + #: Whether the entity is a person or not. + is_person: ClassVar[bool] = False + def __init__( self, key: str, @@ -38,19 +42,18 @@ def __init__( containing_entities: Iterable[str] = (), ) -> None: self.key = t.EntityKey(key) - self.label = label self.plural = t.EntityPlural(plural) + self.label = label self.doc = textwrap.dedent(doc) - self.is_person = False self.roles_description = roles self.roles: Iterable[Role] = () for role_description in roles: role = Role(role_description, self) setattr(self, role.key.upper(), role) self.roles = (*self.roles, role) - if role_description.get("subroles"): + if subroles := role_description.get("subroles"): role.subroles = () - for subrole_key in role_description["subroles"]: + for subrole_key in subroles: subrole = Role({"key": subrole_key, "max": 1}, self) setattr(self, subrole.key.upper(), subrole) role.subroles = (*role.subroles, subrole) @@ -58,6 +61,7 @@ def __init__( self.flattened_roles = tuple( chain.from_iterable(role.subroles or [role] for role in self.roles), ) - - self.is_person = False self.containing_entities = containing_entities + + +__all__ = ["GroupEntity"] diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index d5ba6cc6a0..808ff0e61a 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -3,7 +3,7 @@ from collections.abc import Iterable, Sequence from . import types as t -from .entity import Entity +from .entity import Entity as SingleEntity from .group_entity import GroupEntity @@ -12,9 +12,9 @@ def build_entity( plural: str, label: str, doc: str = "", - roles: Sequence[t.RoleParams] | None = None, + roles: None | Sequence[t.RoleParams] = None, is_person: bool = False, - class_override: object | None = None, + class_override: object = None, containing_entities: Sequence[str] = (), ) -> t.SingleEntity | t.GroupEntity: """Build a SingleEntity or a GroupEntity. @@ -69,8 +69,9 @@ def build_entity( TypeError: 'Role' object is not iterable """ + if is_person: - return Entity(key, plural, label, doc) + return SingleEntity(key, plural, label, doc) if roles is not None: return GroupEntity( @@ -89,8 +90,8 @@ def find_role( roles: Iterable[t.Role], key: t.RoleKey, *, - total: int | None = None, -) -> t.Role | None: + total: None | int = None, +) -> None | t.Role: """Find a Role in a GroupEntity. Args: @@ -157,3 +158,6 @@ def find_role( return role return None + + +__all__ = ["build_entity", "find_role"] diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 45193fffc0..0cb83cde53 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,12 +1,9 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping -from typing import Any +from collections.abc import Iterable -import dataclasses -import textwrap - -from .types import SingleEntity +from . import types as t +from ._description import _Description class Role: @@ -48,44 +45,45 @@ class Role: """ #: The Entity the Role belongs to. - entity: SingleEntity + entity: t.GroupEntity #: A description of the Role. description: _Description #: Max number of members. - max: int | None = None + max: None | int = None #: A list of subroles. - subroles: Iterable[Role] | None = None + subroles: None | Iterable[Role] = None @property - def key(self) -> str: + def key(self) -> t.RoleKey: """A key to identify the Role.""" - return self.description.key + return t.RoleKey(self.description.key) @property - def plural(self) -> str | None: + def plural(self) -> None | t.RolePlural: """The ``key``, pluralised.""" - return self.description.plural + if (plural := self.description.plural) is None: + return None + return t.RolePlural(plural) @property - def label(self) -> str | None: + def label(self) -> None | str: """A summary description.""" return self.description.label @property - def doc(self) -> str | None: + def doc(self) -> None | str: """A full description, non-indented.""" return self.description.doc - def __init__(self, description: Mapping[str, Any], entity: SingleEntity) -> None: + def __init__(self, description: t.RoleParams, entity: t.GroupEntity) -> None: self.description = _Description( - **{ - key: value - for key, value in description.items() - if key in {"key", "plural", "label", "doc"} - }, + key=description["key"], + plural=description.get("plural"), + label=description.get("label"), + doc=description.get("doc"), ) self.entity = entity self.max = description.get("max") @@ -94,51 +92,4 @@ def __repr__(self) -> str: return f"Role({self.key})" -@dataclasses.dataclass(frozen=True) -class _Description: - r"""A Role's description. - - Examples: - >>> data = { - ... "key": "parent", - ... "label": "Parents", - ... "plural": "parents", - ... "doc": "\t\t\tThe one/two adults in charge of the household.", - ... } - - >>> description = _Description(**data) - - >>> repr(_Description) - "" - - >>> repr(description) - "_Description(key='parent', plural='parents', label='Parents', ...)" - - >>> str(description) - "_Description(key='parent', plural='parents', label='Parents', ...)" - - >>> {description} - {_Description(key='parent', plural='parents', label='Parents', doc=...} - - >>> description.key - 'parent' - - .. versionadded:: 41.0.1 - - """ - - #: A key to identify the Role. - key: str - - #: The ``key``, pluralised. - plural: str | None = None - - #: A summary description. - label: str | None = None - - #: A full description, non-indented. - doc: str | None = None - - def __post_init__(self) -> None: - if self.doc is not None: - object.__setattr__(self, "doc", textwrap.dedent(self.doc)) +__all__ = ["Role"] diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index 38607d5488..ef6af9024f 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -1,40 +1,21 @@ -from __future__ import annotations - -from collections.abc import Iterable -from typing import NewType, Protocol from typing_extensions import Required, TypedDict -from openfisca_core import types as t +from openfisca_core.types import ( + CoreEntity, + EntityKey, + EntityPlural, + GroupEntity, + Role, + RoleKey, + RolePlural, + SingleEntity, + TaxBenefitSystem, + Variable, + VariableName, +) # Entities -#: For example "person". -EntityKey = NewType("EntityKey", str) - -#: For example "persons". -EntityPlural = NewType("EntityPlural", str) - -#: For example "principal". -RoleKey = NewType("RoleKey", str) - -#: For example "parents". -RolePlural = NewType("RolePlural", str) - - -class CoreEntity(t.CoreEntity, Protocol): - key: EntityKey - plural: EntityPlural | None - - -class SingleEntity(t.SingleEntity, Protocol): ... - - -class GroupEntity(t.GroupEntity, Protocol): ... - - -class Role(t.Role, Protocol): - subroles: Iterable[Role] | None - class RoleParams(TypedDict, total=False): key: Required[str] @@ -45,13 +26,17 @@ class RoleParams(TypedDict, total=False): subroles: list[str] -# Tax-Benefit systems - - -class TaxBenefitSystem(t.TaxBenefitSystem, Protocol): ... - - -# Variables - - -class Variable(t.Variable, Protocol): ... +__all__ = [ + "CoreEntity", + "EntityKey", + "EntityPlural", + "GroupEntity", + "Role", + "RoleKey", + "RoleParams", + "RolePlural", + "SingleEntity", + "TaxBenefitSystem", + "Variable", + "VariableName", +] diff --git a/openfisca_core/types.py b/openfisca_core/types.py index 5134c518d6..d1c13c1f10 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -2,46 +2,56 @@ from collections.abc import Iterable, Sequence, Sized from numpy.typing import NDArray -from typing import Any, TypeVar, Union +from typing import Any, NewType, TypeVar, Union from typing_extensions import Protocol, TypeAlias import numpy -N = TypeVar("N", bound=numpy.generic, covariant=True) +_N_co = TypeVar("_N_co", bound=numpy.generic, covariant=True) #: Type representing an numpy array. -Array: TypeAlias = NDArray[N] +Array: TypeAlias = NDArray[_N_co] L = TypeVar("L") #: Type representing an array-like object. ArrayLike: TypeAlias = Sequence[L] -#: Type variable representing an error. -E = TypeVar("E", covariant=True) - -#: Type variable representing a value. -A = TypeVar("A", covariant=True) - #: Generic type vars. -T_cov = TypeVar("T_cov", covariant=True) -T_con = TypeVar("T_con", contravariant=True) +_T_co = TypeVar("_T_co", covariant=True) # Entities +#: For example "person". +EntityKey = NewType("EntityKey", str) + +#: For example "persons". +EntityPlural = NewType("EntityPlural", str) + +#: For example "principal". +RoleKey = NewType("RoleKey", str) + +#: For example "parents". +RolePlural = NewType("RolePlural", str) + class CoreEntity(Protocol): - key: Any - plural: Any + key: EntityKey + plural: EntityPlural - def check_role_validity(self, role: Any) -> None: ... - def check_variable_defined_for_entity(self, variable_name: Any) -> None: ... + def check_role_validity(self, role: object, /) -> None: ... + def check_variable_defined_for_entity( + self, + variable_name: VariableName, + /, + ) -> None: ... def get_variable( self, - variable_name: Any, - check_existence: Any = ..., - ) -> Any | None: ... + variable_name: VariableName, + check_existence: bool = ..., + /, + ) -> None | Variable: ... class SingleEntity(CoreEntity, Protocol): ... @@ -51,20 +61,22 @@ class GroupEntity(CoreEntity, Protocol): ... class Role(Protocol): - entity: Any + entity: GroupEntity max: int | None - subroles: Any + subroles: None | Iterable[Role] @property - def key(self) -> str: ... + def key(self, /) -> RoleKey: ... + @property + def plural(self, /) -> None | RolePlural: ... # Holders class Holder(Protocol): - def clone(self, population: Any) -> Holder: ... - def get_memory_usage(self) -> Any: ... + def clone(self, population: Any, /) -> Holder: ... + def get_memory_usage(self, /) -> Any: ... # Parameters @@ -76,12 +88,12 @@ class ParameterNodeAtInstant(Protocol): ... # Periods -class Container(Protocol[T_con]): - def __contains__(self, item: T_con, /) -> bool: ... +class Container(Protocol[_T_co]): + def __contains__(self, item: object, /) -> bool: ... -class Indexable(Protocol[T_cov]): - def __getitem__(self, index: int, /) -> T_cov: ... +class Indexable(Protocol[_T_co]): + def __getitem__(self, index: int, /) -> _T_co: ... class DateUnit(Container[str], Protocol): ... @@ -92,7 +104,7 @@ class Instant(Indexable[int], Iterable[int], Sized, Protocol): ... class Period(Indexable[Union[DateUnit, Instant, int]], Protocol): @property - def unit(self) -> DateUnit: ... + def unit(self, /) -> DateUnit: ... # Populations @@ -101,17 +113,17 @@ def unit(self) -> DateUnit: ... class Population(Protocol): entity: Any - def get_holder(self, variable_name: Any) -> Any: ... + def get_holder(self, variable_name: VariableName, /) -> Any: ... # Simulations class Simulation(Protocol): - def calculate(self, variable_name: Any, period: Any) -> Any: ... - def calculate_add(self, variable_name: Any, period: Any) -> Any: ... - def calculate_divide(self, variable_name: Any, period: Any) -> Any: ... - def get_population(self, plural: Any | None) -> Any: ... + def calculate(self, variable_name: VariableName, period: Any, /) -> Any: ... + def calculate_add(self, variable_name: VariableName, period: Any, /) -> Any: ... + def calculate_divide(self, variable_name: VariableName, period: Any, /) -> Any: ... + def get_population(self, plural: None | str, /) -> Any: ... # Tax-Benefit systems @@ -122,16 +134,21 @@ class TaxBenefitSystem(Protocol): def get_variable( self, - variable_name: Any, - check_existence: Any = ..., - ) -> Any | None: ... + variable_name: VariableName, + check_existence: bool = ..., + /, + ) -> None | Variable: ... # Variables +#: For example "salary". +VariableName = NewType("VariableName", str) + class Variable(Protocol): entity: Any + name: VariableName class Formula(Protocol): @@ -140,8 +157,9 @@ def __call__( population: Population, instant: Instant, params: Params, + /, ) -> Array[Any]: ... class Params(Protocol): - def __call__(self, instant: Instant) -> ParameterNodeAtInstant: ... + def __call__(self, instant: Instant, /) -> ParameterNodeAtInstant: ... diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 8afba8cd97..99b5cba7b7 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -42,7 +42,6 @@ check-types: @mypy \ openfisca_core/commons \ openfisca_core/entities \ - openfisca_core/periods \ openfisca_core/types.py @$(call print_pass,$@:) diff --git a/setup.cfg b/setup.cfg index 7b826f8185..c31d9f9e7e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ include-in-doctest = openfisca_core/projectors max-line-length = 88 per-file-ignores = - */types.py:D101,D102,E301,E704 + */types.py:D101,D102,E301,E704,W504 */test_*.py:D101,D102,D103 */__init__.py:F401 */__init__.pyi:E302,E704 diff --git a/setup.py b/setup.py index 6b573a28b1..ff2b0c4305 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ "dpath >=2.1.4, <3.0", "numexpr >=2.8.4, <3.0", "numpy >=1.24.2, <1.25", - "pendulum >=3.0.0, <4.0.0", + "pendulum >=2.1.2, <3.0.0", "psutil >=5.9.4, <6.0", "pytest >=8.3.3, <9.0", "sortedcontainers >=2.4.0, <3.0", @@ -61,7 +61,7 @@ "openapi-spec-validator >=0.7.1, <0.8.0", "pylint >=3.3.1, <4.0", "pylint-per-file-ignores >=1.3.2, <2.0", - "pyright >=1.1.381, <2.0", + "pyright >=1.1.382, <2.0", "ruff >=0.6.7, <1.0", "ruff-lsp >=0.0.57, <1.0", "xdoctest >=1.2.0, <2.0", diff --git a/stubs/numexpr/__init__.pyi b/stubs/numexpr/__init__.pyi index 6607dd9ee9..f9ada73c3b 100644 --- a/stubs/numexpr/__init__.pyi +++ b/stubs/numexpr/__init__.pyi @@ -1,9 +1,9 @@ -from __future__ import annotations - from numpy.typing import NDArray import numpy def evaluate( - __ex: str, *__args: object, **__kwargs: object + __ex: str, + *__args: object, + **__kwargs: object, ) -> NDArray[numpy.bool_] | NDArray[numpy.int32] | NDArray[numpy.float32]: ... From ecf904f4d4943e5742ca92627c48fd7b94219a8e Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 19 Sep 2024 19:40:50 +0200 Subject: [PATCH 149/188] chore(version): bump --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 002cf14714..93e867efec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.5.7 [#1225](https://github.com/openfisca/openfisca-core/pull/1225) + +#### Technical changes + +- Refactor & test `eval_expression` + ### 41.5.6 [#1185](https://github.com/openfisca/openfisca-core/pull/1185) #### Technical changes diff --git a/setup.py b/setup.py index ff2b0c4305..9b62476a44 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="OpenFisca-Core", - version="41.5.6", + version="41.5.7", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From dd4ca9ff0fec839858633defbfd1f26a97cfd437 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 25 Sep 2024 21:53:26 +0200 Subject: [PATCH 150/188] docs(periods): fix types (#1229) --- openfisca_core/commons/misc.py | 4 +++- openfisca_core/entities/py.typed | 0 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 openfisca_core/entities/py.typed diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 138dd18e1e..357a034847 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -30,10 +30,12 @@ def empty_clone(original: object) -> object: """ + def __init__(_): ... + Dummy = type( "Dummy", (original.__class__,), - {"__init__": lambda _: None}, + {"__init__": __init__}, ) new = Dummy() diff --git a/openfisca_core/entities/py.typed b/openfisca_core/entities/py.typed new file mode 100644 index 0000000000..e69de29bb2 From 93eec6c9939e4fbe214e4acb97b2340102f3c914 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 25 Sep 2024 22:26:01 +0200 Subject: [PATCH 151/188] style(periods): fix linter errors (#1229) --- openfisca_core/periods/_parsers.py | 34 ++++++++++--------- openfisca_core/periods/date_unit.py | 2 -- openfisca_core/periods/helpers.py | 33 +++++++++++------- openfisca_core/periods/instant_.py | 2 ++ .../periods/tests/helpers/test_helpers.py | 6 ++-- .../{test__parsers.py => test_parsers.py} | 6 ++-- 6 files changed, 46 insertions(+), 37 deletions(-) rename openfisca_core/periods/tests/{test__parsers.py => test_parsers.py} (93%) diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index 197a189b58..b694754dad 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -1,4 +1,4 @@ -from typing import Optional +from __future__ import annotations import re @@ -13,22 +13,23 @@ invalid_week = re.compile(r".*(W[1-9]|W[1-9]-[0-9]|W[0-5][0-9]-0)$") -def _parse_period(value: str) -> Optional[t.Period]: +def parse_period(value: str) -> None | t.Period: """Parses ISO format/calendar periods. Such as "2012" or "2015-03". Examples: - >>> _parse_period("2022") + >>> parse_period("2022") Period((, Instant((2022, 1, 1)), 1)) - >>> _parse_period("2022-02") + >>> parse_period("2022-02") Period((, Instant((2022, 2, 1)), 1)) - >>> _parse_period("2022-W02-7") + >>> parse_period("2022-W02-7") Period((, Instant((2022, 1, 16)), 1)) """ + # If it's a complex period, next! if len(value.split(":")) != 1: return None @@ -45,18 +46,18 @@ def _parse_period(value: str) -> Optional[t.Period]: if invalid_week.match(value): raise ParserError - unit = _parse_unit(value) + unit = parse_unit(value) date = pendulum.parse(value, exact=True) if not isinstance(date, pendulum.Date): - raise ValueError + raise TypeError instant = Instant((date.year, date.month, date.day)) return Period((unit, instant, 1)) -def _parse_unit(value: str) -> t.DateUnit: +def parse_unit(value: str) -> t.DateUnit: """Determine the date unit of a date string. Args: @@ -69,33 +70,34 @@ def _parse_unit(value: str) -> t.DateUnit: ValueError when no DateUnit can be determined. Examples: - >>> _parse_unit("2022") + >>> parse_unit("2022") - >>> _parse_unit("2022-W03-01") + >>> parse_unit("2022-W03-01") """ + + format_y, format_ym, format_ymd = 1, 2, 3 length = len(value.split("-")) isweek = value.find("W") != -1 - if length == 1: + if length == format_y: return DateUnit.YEAR - if length == 2: + if length == format_ym: if isweek: return DateUnit.WEEK return DateUnit.MONTH - if length == 3: + if length == format_ymd: if isweek: return DateUnit.WEEKDAY return DateUnit.DAY - else: - raise ValueError + raise ValueError -__all__ = ["_parse_period", "_parse_unit"] +__all__ = ["parse_period", "parse_unit"] diff --git a/openfisca_core/periods/date_unit.py b/openfisca_core/periods/date_unit.py index 9d981f6462..c66346c3c2 100644 --- a/openfisca_core/periods/date_unit.py +++ b/openfisca_core/periods/date_unit.py @@ -92,8 +92,6 @@ class DateUnit(StrEnum, metaclass=DateUnitMeta): >>> DateUnit.DAY.value 'day' - .. versionadded:: 35.9.0 - """ def __contains__(self, other: object) -> bool: diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 62f51570d7..87304c470d 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -7,8 +7,9 @@ import pendulum -from . import _parsers, config, types as t +from . import config, types as t from ._errors import ParserError +from ._parsers import parse_period from .date_unit import DateUnit from .instant_ import Instant from .period_ import Period @@ -64,6 +65,7 @@ def instant(instant: object) -> None | t.Instant: """ result: t.Instant | tuple[int, ...] + format_y, format_ym, format_ymd = 1, 2, 3 if instant is None: return None @@ -71,7 +73,10 @@ def instant(instant: object) -> None | t.Instant: return instant if isinstance(instant, str): if not config.INSTANT_PATTERN.match(instant): - msg = f"'{instant}' is not a valid instant. Instants are described using the 'YYYY-MM-DD' format, for instance '2015-06-15'." + msg = ( + f"'{instant}' is not a valid instant. Instants are described " + "using the 'YYYY-MM-DD' format, for instance '2015-06-15'." + ) raise ValueError( msg, ) @@ -81,17 +86,17 @@ def instant(instant: object) -> None | t.Instant: elif isinstance(instant, int): result = (instant,) elif isinstance(instant, list): - assert 1 <= len(instant) <= 3 + assert 1 <= len(instant) <= format_ymd result = tuple(instant) elif isinstance(instant, Period): result = instant.start else: assert isinstance(instant, tuple), instant - assert 1 <= len(instant) <= 3 + assert format_y <= len(instant) <= format_ymd result = instant - if len(result) == 1: + if len(result) == format_y: return Instant((result[0], 1, 1)) - if len(result) == 2: + if len(result) == format_ym: return Instant((result[0], result[1], 1)) return Instant(result) @@ -165,8 +170,10 @@ def period(value: object) -> t.Period: >>> period("day:2014-02-02:3") Period((, Instant((2014, 2, 2)), 3)) - """ + + format_ym, format_ymd = 2, 3 + if isinstance(value, Period): return value @@ -199,7 +206,7 @@ def period(value: object) -> t.Period: # Try to parse from an ISO format/calendar period. try: - period = _parsers._parse_period(value) + period = parse_period(value) except (AttributeError, ValueError, ParserError): _raise_error(value) @@ -224,7 +231,7 @@ def period(value: object) -> t.Period: # middle component must be a valid iso period try: - base_period = _parsers._parse_period(components[1]) + base_period = parse_period(components[1]) except (AttributeError, ValueError, ParserError): _raise_error(value) @@ -233,11 +240,11 @@ def period(value: object) -> t.Period: _raise_error(value) # period like year:2015-03 have a size of 1 - if len(components) == 2: + if len(components) == format_ym: size = 1 # if provided, make sure the size is an integer - elif len(components) == 3: + elif len(components) == format_ymd: try: size = int(components[2]) @@ -268,8 +275,8 @@ def _raise_error(value: str) -> NoReturn: """ message = os.linesep.join( [ - f"Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '{value}'.", - "Learn more about legal period formats in OpenFisca:", + "Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); ", + f"got: '{value}'. Learn more about legal period formats in OpenFisca:", ".", ], ) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 752a947bd3..9d26dc815a 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -76,6 +76,8 @@ class Instant(tuple[int, int, int]): """ + __slots__ = () + def __repr__(self) -> str: return f"{self.__class__.__name__}({super().__repr__()})" diff --git a/openfisca_core/periods/tests/helpers/test_helpers.py b/openfisca_core/periods/tests/helpers/test_helpers.py index 8998298685..175ea8c873 100644 --- a/openfisca_core/periods/tests/helpers/test_helpers.py +++ b/openfisca_core/periods/tests/helpers/test_helpers.py @@ -54,10 +54,10 @@ def test_key_period_size(arg, expected) -> None: @pytest.mark.parametrize( - "arg, error", + ("arg", "error"), [ - [(DateUnit.DAY, None, 1), AttributeError], - [(DateUnit.MONTH, None, -1000), AttributeError], + ((DateUnit.DAY, None, 1), AttributeError), + ((DateUnit.MONTH, None, -1000), AttributeError), ], ) def test_key_period_size_when_an_invalid_argument(arg, error): diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test_parsers.py similarity index 93% rename from openfisca_core/periods/tests/test__parsers.py rename to openfisca_core/periods/tests/test_parsers.py index 3f47f5abc5..6a3781b5e5 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test_parsers.py @@ -16,7 +16,7 @@ ], ) def test__parse_period(arg, expected) -> None: - assert _parsers._parse_period(arg) == expected + assert _parsers.parse_period(arg) == expected @pytest.mark.parametrize( @@ -51,7 +51,7 @@ def test__parse_period(arg, expected) -> None: ) def test__parse_period_with_invalid_argument(arg, error) -> None: with pytest.raises(error): - _parsers._parse_period(arg) + _parsers.parse_period(arg) @pytest.mark.parametrize( @@ -65,4 +65,4 @@ def test__parse_period_with_invalid_argument(arg, error) -> None: ], ) def test__parse_unit(arg, expected) -> None: - assert _parsers._parse_unit(arg) == expected + assert _parsers.parse_unit(arg) == expected From 1ef1d912ce4b06409282e29fb2cbbf013892ccd1 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 26 Sep 2024 13:33:08 +0200 Subject: [PATCH 152/188] refactor(periods)!: fix instant complexity (#1229) Method `periods.instant` had way too much branching and cyclomatic complexity. It has been refactored so to make use of `functools.singledispatch` to improve readability and testing. BREAKING CHANGE: `periods.instant` no longer returns `None`. Now, it raises `periods.InstantError` instead. --- CHANGELOG.md | 12 +- openfisca_core/periods/__init__.py | 33 ++--- openfisca_core/periods/_errors.py | 14 ++- openfisca_core/periods/helpers.py | 116 ++++++++++-------- .../periods/tests/helpers/test_instant.py | 35 +++--- openfisca_core/periods/types.py | 23 +++- 6 files changed, 142 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 305912f918..a59259f40f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,22 @@ # Changelog -## 41.6.0 [#1223](https://github.com/openfisca/openfisca-core/pull/1223) +# 42.0.0 [#1223](https://github.com/openfisca/openfisca-core/pull/1223) + +#### Breaking changes + +- `periods.instant` no longer returns `None` + - Now, it raises `periods.InstantError` #### New features - Introduce `Instant.eternity()` + - This behaviour was duplicated across + - Now it is encapsulated in a single method #### Technical changes -- Update `pendulum' +- Update `pendulum` +- Reduce code complexity - Remove run-time type-checks - Add typing to the periods module diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 60300f09c8..93de24d621 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -22,7 +22,7 @@ # See: https://www.python.org/dev/peps/pep-0008/#imports from . import types -from ._errors import ParserError +from ._errors import InstantError, ParserError from .config import ( INSTANT_PATTERN, date_by_instant_cache, @@ -51,27 +51,28 @@ ISOCALENDAR = DateUnit.isocalendar __all__ = [ + "DAY", + "DateUnit", + "ETERNITY", "INSTANT_PATTERN", + "ISOCALENDAR", + "ISOFORMAT", + "Instant", + "InstantError", + "MONTH", + "ParserError", + "Period", + "WEEK", + "WEEKDAY", + "YEAR", "date_by_instant_cache", - "str_by_instant_cache", - "year_or_month_or_day_re", - "DateUnit", "instant", "instant_date", "key_period_size", "period", + "str_by_instant_cache", + "types", "unit_weight", "unit_weights", - "Instant", - "Period", - "WEEKDAY", - "WEEK", - "DAY", - "MONTH", - "YEAR", - "ETERNITY", - "ISOFORMAT", - "ISOCALENDAR", - "ParserError", - "types", + "year_or_month_or_day_re", ] diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index 4ab3c64597..91453c5b2f 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -1,3 +1,15 @@ from pendulum.parsing.exceptions import ParserError -__all__ = ["ParserError"] + +class InstantError(ValueError): + """Raised when an invalid instant-like is provided.""" + + def __init__(self, value: str) -> None: + msg = ( + f"'{value}' is not a valid instant. Instants are described " + "using the 'YYYY-MM-DD' format, for instance '2015-06-15'." + ) + super().__init__(msg) + + +__all__ = ["InstantError", "ParserError"] diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 87304c470d..374321a964 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -1,41 +1,30 @@ from __future__ import annotations -from typing import NoReturn, overload +from typing import NoReturn import datetime +import functools import os import pendulum from . import config, types as t -from ._errors import ParserError +from ._errors import InstantError, ParserError from ._parsers import parse_period from .date_unit import DateUnit from .instant_ import Instant from .period_ import Period -@overload -def instant(instant: None) -> None: ... - - -@overload -def instant(instant: int | t.Period | t.Instant | datetime.date) -> t.Instant: ... - - -@overload -def instant(instant: str | list[int] | tuple[int, ...]) -> t.Instant: ... - - -def instant(instant: object) -> None | t.Instant: +@functools.singledispatch +def instant(value: object) -> t.Instant: """Build a new instant, aka a triple of integers (year, month, day). Args: - instant: An ``instant-like`` object. + value(object): An ``instant-like`` object. Returns: - None: When ``instant`` is None. - :obj:`.Instant`: Otherwise. + :obj:`.Instant`: A new instant. Raises: :exc:`ValueError`: When the arguments were invalid, like "2021-32-13". @@ -62,43 +51,66 @@ def instant(instant: object) -> None | t.Instant: >>> instant("2021") Instant((2021, 1, 1)) + >>> instant([2021]) + Instant((2021, 1, 1)) + + >>> instant([2021, 9]) + Instant((2021, 9, 1)) + + >>> instant(None) + Traceback (most recent call last): + openfisca_core.periods._errors.InstantError: 'None' is not a valid i... + """ - result: t.Instant | tuple[int, ...] - format_y, format_ym, format_ymd = 1, 2, 3 + if isinstance(value, t.SeqInt): + return instant(t.SeqInt(value)) - if instant is None: - return None - if isinstance(instant, Instant): - return instant - if isinstance(instant, str): - if not config.INSTANT_PATTERN.match(instant): - msg = ( - f"'{instant}' is not a valid instant. Instants are described " - "using the 'YYYY-MM-DD' format, for instance '2015-06-15'." - ) - raise ValueError( - msg, - ) - result = Instant(int(fragment) for fragment in instant.split("-", 2)[:3]) - elif isinstance(instant, datetime.date): - result = Instant((instant.year, instant.month, instant.day)) - elif isinstance(instant, int): - result = (instant,) - elif isinstance(instant, list): - assert 1 <= len(instant) <= format_ymd - result = tuple(instant) - elif isinstance(instant, Period): - result = instant.start - else: - assert isinstance(instant, tuple), instant - assert format_y <= len(instant) <= format_ymd - result = instant - if len(result) == format_y: - return Instant((result[0], 1, 1)) - if len(result) == format_ym: - return Instant((result[0], result[1], 1)) - return Instant(result) + raise InstantError(str(value)) + + +@instant.register +def _(value: None) -> NoReturn: + raise InstantError(str(value)) + + +@instant.register +def _(value: int) -> t.Instant: + return Instant((value, 1, 1)) + + +@instant.register +def _(value: str) -> t.Instant: + # TODO(): Add support for weeks (isocalendar). + # https://github.com/openfisca/openfisca-core/issues/1232 + if not config.INSTANT_PATTERN.match(value): + raise InstantError(value) + return instant(tuple(int(_) for _ in value.split("-", 2)[:3])) + + +@instant.register +def _(value: Period) -> t.Instant: + return value.start + + +@instant.register +def _(value: t.Instant) -> t.Instant: + return value + + +@instant.register +def _(value: datetime.date) -> t.Instant: + return Instant((value.year, value.month, value.day)) + + +@instant.register +def _(value: pendulum.Date) -> t.Instant: + return Instant((value.year, value.month, value.day)) + + +@instant.register +def _(value: t.SeqInt) -> t.Instant: + return Instant((value + [1] * 3)[:3]) def instant_date(instant: None | t.Instant) -> None | datetime.date: diff --git a/openfisca_core/periods/tests/helpers/test_instant.py b/openfisca_core/periods/tests/helpers/test_instant.py index 73f37ece6f..169df88afd 100644 --- a/openfisca_core/periods/tests/helpers/test_instant.py +++ b/openfisca_core/periods/tests/helpers/test_instant.py @@ -9,7 +9,6 @@ @pytest.mark.parametrize( ("arg", "expected"), [ - (None, None), (datetime.date(1, 1, 1), Instant((1, 1, 1))), (Instant((1, 1, 1)), Instant((1, 1, 1))), (Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))), @@ -21,23 +20,9 @@ ("1000", Instant((1000, 1, 1))), ("1000-01", Instant((1000, 1, 1))), ("1000-01-01", Instant((1000, 1, 1))), - ((None,), Instant((None, 1, 1))), - ((None, None), Instant((None, None, 1))), - ((None, None, None), Instant((None, None, None))), - ((datetime.date(1, 1, 1),), Instant((datetime.date(1, 1, 1), 1, 1))), - ((Instant((1, 1, 1)),), Instant((Instant((1, 1, 1)), 1, 1))), - ( - (Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), - Instant((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), 1, 1)), - ), ((-1,), Instant((-1, 1, 1))), ((-1, -1), Instant((-1, -1, 1))), ((-1, -1, -1), Instant((-1, -1, -1))), - (("-1",), Instant(("-1", 1, 1))), - (("-1", "-1"), Instant(("-1", "-1", 1))), - (("-1", "-1", "-1"), Instant(("-1", "-1", "-1"))), - (("1-1",), Instant(("1-1", 1, 1))), - (("1-1-1",), Instant(("1-1-1", 1, 1))), ], ) def test_instant(arg, expected) -> None: @@ -47,6 +32,7 @@ def test_instant(arg, expected) -> None: @pytest.mark.parametrize( ("arg", "error"), [ + (None, periods.InstantError), (DateUnit.YEAR, ValueError), (DateUnit.ETERNITY, ValueError), ("1000-0", ValueError), @@ -65,10 +51,21 @@ def test_instant(arg, expected) -> None: ("year:1000-01-01:3", ValueError), ("1000-01-01:a", ValueError), ("1000-01-01:1", ValueError), - ((), AssertionError), - ({}, AssertionError), - ("", ValueError), - ((None, None, None, None), AssertionError), + ((), periods.InstantError), + ({}, periods.InstantError), + ("", periods.InstantError), + ((None,), periods.InstantError), + ((None, None), periods.InstantError), + ((None, None, None), periods.InstantError), + ((None, None, None, None), periods.InstantError), + (("-1",), periods.InstantError), + (("-1", "-1"), periods.InstantError), + (("-1", "-1", "-1"), periods.InstantError), + (("1-1",), periods.InstantError), + (("1-1-1",), periods.InstantError), + ((datetime.date(1, 1, 1),), periods.InstantError), + ((Instant((1, 1, 1)),), periods.InstantError), + ((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), periods.InstantError), ], ) def test_instant_with_an_invalid_argument(arg, error) -> None: diff --git a/openfisca_core/periods/types.py b/openfisca_core/periods/types.py index d29092830d..fdcd8ff5ae 100644 --- a/openfisca_core/periods/types.py +++ b/openfisca_core/periods/types.py @@ -1,3 +1,24 @@ +from collections.abc import Sequence + from openfisca_core.types import DateUnit, Instant, InstantStr, Period, PeriodStr -__all__ = ["DateUnit", "Instant", "Period", "InstantStr", "PeriodStr"] + +class _SeqIntMeta(type): + def __instancecheck__(self, arg: Sequence[object]) -> bool: + try: + return bool(arg) and all(isinstance(item, int) for item in arg) + except TypeError: + return False + + +class SeqInt(list[int], metaclass=_SeqIntMeta): ... + + +__all__ = [ + "DateUnit", + "Instant", + "InstantStr", + "Period", + "PeriodStr", + "SeqInt", +] From 33f00cf96d4e110b89e47598ea1e8fa5fd932798 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 26 Sep 2024 14:47:14 +0200 Subject: [PATCH 153/188] refactor(periods): ensure is str in parse (#1229) Broaden the expected argument type to be of generic type `object`, and do an explicit casting to help users know that the expected argument for that function is a string. --- openfisca_core/periods/_parsers.py | 7 ++++++- openfisca_core/periods/tests/test_parsers.py | 10 +++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index b694754dad..23c5fc5811 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -13,7 +13,7 @@ invalid_week = re.compile(r".*(W[1-9]|W[1-9]-[0-9]|W[0-5][0-9]-0)$") -def parse_period(value: str) -> None | t.Period: +def parse_period(value: object) -> None | t.Period: """Parses ISO format/calendar periods. Such as "2012" or "2015-03". @@ -30,6 +30,11 @@ def parse_period(value: str) -> None | t.Period: """ + # If it is not a string, next! + if not isinstance(value, str): + msg = f"Expected a {str.__name__}, got {type(value).__name__}." + raise NotImplementedError(msg) + # If it's a complex period, next! if len(value.split(":")) != 1: return None diff --git a/openfisca_core/periods/tests/test_parsers.py b/openfisca_core/periods/tests/test_parsers.py index 6a3781b5e5..92f11dda45 100644 --- a/openfisca_core/periods/tests/test_parsers.py +++ b/openfisca_core/periods/tests/test_parsers.py @@ -22,11 +22,11 @@ def test__parse_period(arg, expected) -> None: @pytest.mark.parametrize( ("arg", "error"), [ - (None, AttributeError), - ({}, AttributeError), - ((), AttributeError), - ([], AttributeError), - (1, AttributeError), + (None, NotImplementedError), + ({}, NotImplementedError), + ((), NotImplementedError), + ([], NotImplementedError), + (1, NotImplementedError), ("", AttributeError), ("à", ParserError), ("1", ValueError), From d5ef3218ffc792afd98e7aef65ef27d39f8712eb Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 26 Sep 2024 17:47:52 +0200 Subject: [PATCH 154/188] style(periods): fix linter errors (#1229) --- openfisca_core/periods/period_.py | 38 +++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index a55d6635b3..87e8465d37 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -117,6 +117,8 @@ class Period(tuple[t.DateUnit, t.Instant, int]): """ + __slots__ = () + def __repr__(self) -> str: return f"{self.__class__.__name__}({super().__repr__()})" @@ -466,11 +468,11 @@ def get_subperiods(self, unit: t.DateUnit) -> Sequence[t.Period]: Examples: >>> period = Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)) >>> period.get_subperiods(DateUnit.MONTH) - [Period((, Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] + [Period((, Instant((2021, 1, 1)), 1)),...] >>> period = Period((DateUnit.YEAR, Instant((2021, 1, 1)), 2)) >>> period.get_subperiods(DateUnit.YEAR) - [Period((, Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] + [Period((, Instant((2021, 1, 1)), 1)), P...] """ if helpers.unit_weight(self.unit) < helpers.unit_weight(unit): @@ -513,19 +515,27 @@ def offset(self, offset: str | int, unit: t.DateUnit | None = None) -> t.Period: >>> Period((DateUnit.DAY, Instant((2021, 1, 1)), 365)).offset(1) Period((, Instant((2021, 1, 2)), 365)) - >>> Period((DateUnit.DAY, Instant((2021, 1, 1)), 365)).offset(1, DateUnit.DAY) + >>> Period((DateUnit.DAY, Instant((2021, 1, 1)), 365)).offset( + ... 1, DateUnit.DAY + ... ) Period((, Instant((2021, 1, 2)), 365)) - >>> Period((DateUnit.DAY, Instant((2021, 1, 1)), 365)).offset(1, DateUnit.MONTH) + >>> Period((DateUnit.DAY, Instant((2021, 1, 1)), 365)).offset( + ... 1, DateUnit.MONTH + ... ) Period((, Instant((2021, 2, 1)), 365)) - >>> Period((DateUnit.DAY, Instant((2021, 1, 1)), 365)).offset(1, DateUnit.YEAR) + >>> Period((DateUnit.DAY, Instant((2021, 1, 1)), 365)).offset( + ... 1, DateUnit.YEAR + ... ) Period((, Instant((2022, 1, 1)), 365)) >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset(1) Period((, Instant((2021, 2, 1)), 12)) - >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset(1, DateUnit.DAY) + >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset( + ... 1, DateUnit.DAY + ... ) Period((, Instant((2021, 1, 2)), 12)) >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset( @@ -533,19 +543,27 @@ def offset(self, offset: str | int, unit: t.DateUnit | None = None) -> t.Period: ... ) Period((, Instant((2021, 2, 1)), 12)) - >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset(1, DateUnit.YEAR) + >>> Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12)).offset( + ... 1, DateUnit.YEAR + ... ) Period((, Instant((2022, 1, 1)), 12)) >>> Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)).offset(1) Period((, Instant((2022, 1, 1)), 1)) - >>> Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)).offset(1, DateUnit.DAY) + >>> Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)).offset( + ... 1, DateUnit.DAY + ... ) Period((, Instant((2021, 1, 2)), 1)) - >>> Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)).offset(1, DateUnit.MONTH) + >>> Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)).offset( + ... 1, DateUnit.MONTH + ... ) Period((, Instant((2021, 2, 1)), 1)) - >>> Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)).offset(1, DateUnit.YEAR) + >>> Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)).offset( + ... 1, DateUnit.YEAR + ... ) Period((, Instant((2022, 1, 1)), 1)) >>> Period((DateUnit.DAY, Instant((2011, 2, 28)), 1)).offset(1) From 022e2c46b810547cfa1aa04198abaa685fda3798 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 26 Sep 2024 22:09:45 +0200 Subject: [PATCH 155/188] feat(periods): parse week instants (#1232) fixes #1232 --- CHANGELOG.md | 2 + openfisca_core/commons/misc.py | 3 +- openfisca_core/periods/__init__.py | 3 +- openfisca_core/periods/_errors.py | 19 +- openfisca_core/periods/_parsers.py | 111 ++++++----- openfisca_core/periods/helpers.py | 154 ++++++---------- .../periods/tests/helpers/test_instant.py | 34 ++-- .../periods/tests/helpers/test_period.py | 110 +++++------ openfisca_core/periods/tests/test_parsers.py | 119 +++++++++--- openfisca_core/periods/types.py | 173 +++++++++++++++++- 10 files changed, 465 insertions(+), 263 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a59259f40f..70d03660ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - Introduce `Instant.eternity()` - This behaviour was duplicated across - Now it is encapsulated in a single method +- Now `periods.instant` parses also ISO calendar strings (weeks) + - For instance, `2022-W01` is now a valid input #### Technical changes diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 357a034847..93e50c6d90 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -30,7 +30,7 @@ def empty_clone(original: object) -> object: """ - def __init__(_): ... + def __init__(_: object) -> None: ... Dummy = type( "Dummy", @@ -71,6 +71,7 @@ def stringify_array(array: None | t.Array[numpy.generic]) -> str: "[, {}, None: msg = ( - f"'{value}' is not a valid instant. Instants are described " - "using the 'YYYY-MM-DD' format, for instance '2015-06-15'." + f"'{value}' is not a valid instant string. Instants are described " + "using either the 'YYYY-MM-DD' format, for instance '2015-06-15', " + "or the 'YYYY-Www-D' format, for instance '2015-W24-1'." ) super().__init__(msg) -__all__ = ["InstantError", "ParserError"] +class PeriodError(ValueError): + """Raised when an invalid period-like is provided.""" + + def __init__(self, value: str) -> None: + msg = ( + "Expected a period (eg. '2017', 'month:2017-01', 'week:2017-W01-1:3', " + f"...); got: '{value}'. Learn more about legal period formats in " + "OpenFisca: ." + ) + super().__init__(msg) + + +__all__ = ["InstantError", "ParserError", "PeriodError"] diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index 23c5fc5811..9973b890a0 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -1,19 +1,64 @@ +"""To parse periods and instants from strings.""" + from __future__ import annotations -import re +import datetime import pendulum from . import types as t -from ._errors import ParserError +from ._errors import InstantError, ParserError, PeriodError from .date_unit import DateUnit from .instant_ import Instant from .period_ import Period -invalid_week = re.compile(r".*(W[1-9]|W[1-9]-[0-9]|W[0-5][0-9]-0)$") +def parse_instant(value: str) -> t.Instant: + """Parse a string into an instant. + + Args: + value (str): The string to parse. + + Returns: + An InstantStr. + + Raises: + InstantError: When the string is not a valid ISO Calendar/Format. + ParserError: When the string couldn't be parsed. + + Examples: + >>> parse_instant("2022") + Instant((2022, 1, 1)) + + >>> parse_instant("2022-02") + Instant((2022, 2, 1)) + + >>> parse_instant("2022-W02-7") + Instant((2022, 1, 16)) + + >>> parse_instant("2022-W013") + Traceback (most recent call last): + openfisca_core.periods._errors.InstantError: '2022-W013' is not a va... + + >>> parse_instant("2022-02-29") + Traceback (most recent call last): + pendulum.parsing.exceptions.ParserError: Unable to parse string [202... + + """ + + if not isinstance(value, t.InstantStr): + raise InstantError(str(value)) + + date = pendulum.parse(value, exact=True) + + if not isinstance(date, datetime.date): + msg = f"Unable to parse string [{value}]" + raise ParserError(msg) + + return Instant((date.year, date.month, date.day)) -def parse_period(value: object) -> None | t.Period: + +def parse_period(value: str) -> t.Period: """Parses ISO format/calendar periods. Such as "2012" or "2015-03". @@ -30,34 +75,13 @@ def parse_period(value: object) -> None | t.Period: """ - # If it is not a string, next! - if not isinstance(value, str): - msg = f"Expected a {str.__name__}, got {type(value).__name__}." - raise NotImplementedError(msg) - - # If it's a complex period, next! - if len(value.split(":")) != 1: - return None - - # Check for a non-empty string. - if len(value) == 0: - raise AttributeError - - # If it's negative, next! - if value[0] == "-": - raise ValueError + try: + instant = parse_instant(value) - # If it's an invalid week, next! - if invalid_week.match(value): - raise ParserError + except InstantError as error: + raise PeriodError(value) from error unit = parse_unit(value) - date = pendulum.parse(value, exact=True) - - if not isinstance(date, pendulum.Date): - raise TypeError - - instant = Instant((date.year, date.month, date.day)) return Period((unit, instant, 1)) @@ -72,37 +96,26 @@ def parse_unit(value: str) -> t.DateUnit: A DateUnit. Raises: - ValueError when no DateUnit can be determined. + InstantError: when no DateUnit can be determined. Examples: >>> parse_unit("2022") - >>> parse_unit("2022-W03-01") + >>> parse_unit("2022-W03-1") """ - format_y, format_ym, format_ymd = 1, 2, 3 - length = len(value.split("-")) - isweek = value.find("W") != -1 - - if length == format_y: - return DateUnit.YEAR - - if length == format_ym: - if isweek: - return DateUnit.WEEK + if not isinstance(value, t.InstantStr): + raise InstantError(str(value)) - return DateUnit.MONTH - - if length == format_ymd: - if isweek: - return DateUnit.WEEKDAY + length = len(value.split("-")) - return DateUnit.DAY + if isinstance(value, t.ISOCalendarStr): + return DateUnit.isocalendar[-length] - raise ValueError + return DateUnit.isoformat[-length] -__all__ = ["parse_period", "parse_unit"] +__all__ = ["parse_instant", "parse_period", "parse_unit"] diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 374321a964..6446c0c839 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -4,13 +4,12 @@ import datetime import functools -import os import pendulum from . import config, types as t -from ._errors import InstantError, ParserError -from ._parsers import parse_period +from ._errors import InstantError, PeriodError +from ._parsers import parse_instant, parse_period from .date_unit import DateUnit from .instant_ import Instant from .period_ import Period @@ -64,7 +63,7 @@ def instant(value: object) -> t.Instant: """ if isinstance(value, t.SeqInt): - return instant(t.SeqInt(value)) + return Instant((list(value) + [1] * 3)[:3]) raise InstantError(str(value)) @@ -79,15 +78,6 @@ def _(value: int) -> t.Instant: return Instant((value, 1, 1)) -@instant.register -def _(value: str) -> t.Instant: - # TODO(): Add support for weeks (isocalendar). - # https://github.com/openfisca/openfisca-core/issues/1232 - if not config.INSTANT_PATTERN.match(value): - raise InstantError(value) - return instant(tuple(int(_) for _ in value.split("-", 2)[:3])) - - @instant.register def _(value: Period) -> t.Instant: return value.start @@ -104,13 +94,8 @@ def _(value: datetime.date) -> t.Instant: @instant.register -def _(value: pendulum.Date) -> t.Instant: - return Instant((value.year, value.month, value.day)) - - -@instant.register -def _(value: t.SeqInt) -> t.Instant: - return Instant((value + [1] * 3)[:3]) +def _(value: str) -> t.Instant: + return parse_instant(value) def instant_date(instant: None | t.Instant) -> None | datetime.date: @@ -139,6 +124,7 @@ def instant_date(instant: None | t.Instant) -> None | datetime.date: return instant_date +@functools.singledispatch def period(value: object) -> t.Period: """Build a new period, aka a triple (unit, start_instant, size). @@ -184,115 +170,81 @@ def period(value: object) -> t.Period: """ - format_ym, format_ymd = 2, 3 - - if isinstance(value, Period): - return value - - # We return a "day-period", for example - # ``, 1))>``. - if isinstance(value, Instant): - return Period((DateUnit.DAY, value, 1)) - - # For example ``datetime.date(2021, 9, 16)``. - if isinstance(value, datetime.date): - return Period((DateUnit.DAY, instant(value), 1)) + one, two, three = 1, 2, 3 # We return an "eternity-period", for example # ``, 0))>``. if str(value).lower() == DateUnit.ETERNITY: return Period.eternity() - # For example ``2021`` gives - # ``, 1))>``. - if isinstance(value, int): - return Period((DateUnit.YEAR, instant(value), 1)) - - # Up to this point, if ``value`` is not a :obj:`str`, we desist. - if not isinstance(value, str): - _raise_error(str(value)) + # We try to parse from an ISO format/calendar period. + if isinstance(value, t.InstantStr): + return parse_period(value) - # There can't be empty strings. - if not value: - _raise_error(value) + # A complex period has a ':' in its string. + if isinstance(value, t.PeriodStr): + components = value.split(":") - # Try to parse from an ISO format/calendar period. - try: - period = parse_period(value) + # The left-most component must be a valid unit + unit = components[0] - except (AttributeError, ValueError, ParserError): - _raise_error(value) + if unit not in list(DateUnit) or unit == DateUnit.ETERNITY: + raise PeriodError(str(value)) - if period is not None: - return period + # Cast ``unit`` to DateUnit. + unit = DateUnit(unit) - # A complex period has a ':' in its string. - if ":" not in value: - _raise_error(value) + # The middle component must be a valid iso period + period = parse_period(components[1]) - components = value.split(":") + # Periods like year:2015-03 have a size of 1 + if len(components) == two: + size = one - # left-most component must be a valid unit - unit = components[0] + # if provided, make sure the size is an integer + elif len(components) == three: + try: + size = int(components[2]) - if unit not in list(DateUnit) or unit == DateUnit.ETERNITY: - _raise_error(value) + except ValueError as error: + raise PeriodError(str(value)) from error - # Cast ``unit`` to DateUnit. - unit = DateUnit(unit) + # If there are more than 2 ":" in the string, the period is invalid + else: + raise PeriodError(str(value)) - # middle component must be a valid iso period - try: - base_period = parse_period(components[1]) + # Reject ambiguous periods such as month:2014 + if unit_weight(period.unit) > unit_weight(unit): + raise PeriodError(str(value)) - except (AttributeError, ValueError, ParserError): - _raise_error(value) + return Period((unit, period.start, size)) - if not base_period: - _raise_error(value) + raise PeriodError(str(value)) - # period like year:2015-03 have a size of 1 - if len(components) == format_ym: - size = 1 - # if provided, make sure the size is an integer - elif len(components) == format_ymd: - try: - size = int(components[2]) +@period.register +def _(value: None) -> NoReturn: + raise PeriodError(str(value)) - except ValueError: - _raise_error(value) - # if there is more than 2 ":" in the string, the period is invalid - else: - _raise_error(value) +@period.register +def _(value: int) -> t.Period: + return Period((DateUnit.YEAR, instant(value), 1)) - # reject ambiguous periods such as month:2014 - if unit_weight(base_period.unit) > unit_weight(unit): - _raise_error(value) - return Period((unit, base_period.start, size)) +@period.register +def _(value: t.Period) -> t.Period: + return value -def _raise_error(value: str) -> NoReturn: - """Raise an error. +@period.register +def _(value: t.Instant) -> t.Period: + return Period((DateUnit.DAY, value, 1)) - Examples: - >>> _raise_error("Oi mate!") - Traceback (most recent call last): - ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ... - Learn more about legal period formats in OpenFisca: - .", - ], - ) - raise ValueError(message) +@period.register +def _(value: datetime.date) -> t.Period: + return Period((DateUnit.DAY, instant(value), 1)) def key_period_size(period: t.Period) -> str: diff --git a/openfisca_core/periods/tests/helpers/test_instant.py b/openfisca_core/periods/tests/helpers/test_instant.py index 169df88afd..fb4472814b 100644 --- a/openfisca_core/periods/tests/helpers/test_instant.py +++ b/openfisca_core/periods/tests/helpers/test_instant.py @@ -3,7 +3,7 @@ import pytest from openfisca_core import periods -from openfisca_core.periods import DateUnit, Instant, Period +from openfisca_core.periods import DateUnit, Instant, InstantError, Period @pytest.mark.parametrize( @@ -32,7 +32,7 @@ def test_instant(arg, expected) -> None: @pytest.mark.parametrize( ("arg", "error"), [ - (None, periods.InstantError), + (None, InstantError), (DateUnit.YEAR, ValueError), (DateUnit.ETERNITY, ValueError), ("1000-0", ValueError), @@ -51,21 +51,21 @@ def test_instant(arg, expected) -> None: ("year:1000-01-01:3", ValueError), ("1000-01-01:a", ValueError), ("1000-01-01:1", ValueError), - ((), periods.InstantError), - ({}, periods.InstantError), - ("", periods.InstantError), - ((None,), periods.InstantError), - ((None, None), periods.InstantError), - ((None, None, None), periods.InstantError), - ((None, None, None, None), periods.InstantError), - (("-1",), periods.InstantError), - (("-1", "-1"), periods.InstantError), - (("-1", "-1", "-1"), periods.InstantError), - (("1-1",), periods.InstantError), - (("1-1-1",), periods.InstantError), - ((datetime.date(1, 1, 1),), periods.InstantError), - ((Instant((1, 1, 1)),), periods.InstantError), - ((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), periods.InstantError), + ((), InstantError), + ({}, InstantError), + ("", InstantError), + ((None,), InstantError), + ((None, None), InstantError), + ((None, None, None), InstantError), + ((None, None, None, None), InstantError), + (("-1",), InstantError), + (("-1", "-1"), InstantError), + (("-1", "-1", "-1"), InstantError), + (("1-1",), InstantError), + (("1-1-1",), InstantError), + ((datetime.date(1, 1, 1),), InstantError), + ((Instant((1, 1, 1)),), InstantError), + ((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), InstantError), ], ) def test_instant_with_an_invalid_argument(arg, error) -> None: diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py index 6973f89746..5f5e345bcc 100644 --- a/openfisca_core/periods/tests/helpers/test_period.py +++ b/openfisca_core/periods/tests/helpers/test_period.py @@ -3,7 +3,7 @@ import pytest from openfisca_core import periods -from openfisca_core.periods import DateUnit, Instant, Period +from openfisca_core.periods import DateUnit, Instant, Period, PeriodError @pytest.mark.parametrize( @@ -73,60 +73,60 @@ def test_period(arg, expected) -> None: @pytest.mark.parametrize( ("arg", "error"), [ - (None, ValueError), - (DateUnit.YEAR, ValueError), - ("1", ValueError), - ("999", ValueError), - ("1000-0", ValueError), - ("1000-13", ValueError), - ("1000-W0", ValueError), - ("1000-W54", ValueError), - ("1000-0-0", ValueError), - ("1000-1-0", ValueError), - ("1000-2-31", ValueError), - ("1000-W0-0", ValueError), - ("1000-W1-0", ValueError), - ("1000-W1-8", ValueError), - ("a", ValueError), - ("year", ValueError), - ("1:1000", ValueError), - ("a:1000", ValueError), - ("month:1000", ValueError), - ("week:1000", ValueError), - ("day:1000-01", ValueError), - ("weekday:1000-W1", ValueError), - ("1000:a", ValueError), - ("1000:1", ValueError), - ("1000-01:1", ValueError), - ("1000-01-01:1", ValueError), - ("1000-W1:1", ValueError), - ("1000-W1-1:1", ValueError), - ("month:1000:1", ValueError), - ("week:1000:1", ValueError), - ("day:1000:1", ValueError), - ("day:1000-01:1", ValueError), - ("weekday:1000:1", ValueError), - ("weekday:1000-W1:1", ValueError), - ((), ValueError), - ({}, ValueError), - ("", ValueError), - ((None,), ValueError), - ((None, None), ValueError), - ((None, None, None), ValueError), - ((None, None, None, None), ValueError), - ((Instant((1, 1, 1)),), ValueError), - ((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), ValueError), - ((1,), ValueError), - ((1, 1), ValueError), - ((1, 1, 1), ValueError), - ((-1,), ValueError), - ((-1, -1), ValueError), - ((-1, -1, -1), ValueError), - (("-1",), ValueError), - (("-1", "-1"), ValueError), - (("-1", "-1", "-1"), ValueError), - (("1-1",), ValueError), - (("1-1-1",), ValueError), + (None, PeriodError), + (DateUnit.YEAR, PeriodError), + ("1", PeriodError), + ("999", PeriodError), + ("1000-0", PeriodError), + ("1000-13", PeriodError), + ("1000-W0", PeriodError), + ("1000-W54", PeriodError), + ("1000-0-0", PeriodError), + ("1000-1-0", PeriodError), + ("1000-2-31", PeriodError), + ("1000-W0-0", PeriodError), + ("1000-W1-0", PeriodError), + ("1000-W1-8", PeriodError), + ("a", PeriodError), + ("year", PeriodError), + ("1:1000", PeriodError), + ("a:1000", PeriodError), + ("month:1000", PeriodError), + ("week:1000", PeriodError), + ("day:1000-01", PeriodError), + ("weekday:1000-W1", PeriodError), + ("1000:a", PeriodError), + ("1000:1", PeriodError), + ("1000-01:1", PeriodError), + ("1000-01-01:1", PeriodError), + ("1000-W1:1", PeriodError), + ("1000-W1-1:1", PeriodError), + ("month:1000:1", PeriodError), + ("week:1000:1", PeriodError), + ("day:1000:1", PeriodError), + ("day:1000-01:1", PeriodError), + ("weekday:1000:1", PeriodError), + ("weekday:1000-W1:1", PeriodError), + ((), PeriodError), + ({}, PeriodError), + ("", PeriodError), + ((None,), PeriodError), + ((None, None), PeriodError), + ((None, None, None), PeriodError), + ((None, None, None, None), PeriodError), + ((Instant((1, 1, 1)),), PeriodError), + ((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), PeriodError), + ((1,), PeriodError), + ((1, 1), PeriodError), + ((1, 1, 1), PeriodError), + ((-1,), PeriodError), + ((-1, -1), PeriodError), + ((-1, -1, -1), PeriodError), + (("-1",), PeriodError), + (("-1", "-1"), PeriodError), + (("-1", "-1", "-1"), PeriodError), + (("1-1",), PeriodError), + (("1-1-1",), PeriodError), ], ) def test_period_with_an_invalid_argument(arg, error) -> None: diff --git a/openfisca_core/periods/tests/test_parsers.py b/openfisca_core/periods/tests/test_parsers.py index 92f11dda45..c9131414b2 100644 --- a/openfisca_core/periods/tests/test_parsers.py +++ b/openfisca_core/periods/tests/test_parsers.py @@ -1,6 +1,67 @@ import pytest -from openfisca_core.periods import DateUnit, Instant, ParserError, Period, _parsers +from openfisca_core.periods import ( + DateUnit, + Instant, + InstantError, + ParserError, + Period, + PeriodError, + _parsers, +) + + +@pytest.mark.parametrize( + ("arg", "expected"), + [ + ("1001", Instant((1001, 1, 1))), + ("1001-01", Instant((1001, 1, 1))), + ("1001-12", Instant((1001, 12, 1))), + ("1001-01-01", Instant((1001, 1, 1))), + ("2028-02-29", Instant((2028, 2, 29))), + ("1001-W01", Instant((1000, 12, 29))), + ("1001-W52", Instant((1001, 12, 21))), + ("1001-W01-1", Instant((1000, 12, 29))), + ], +) +def test_parse_instant(arg, expected) -> None: + assert _parsers.parse_instant(arg) == expected + + +@pytest.mark.parametrize( + ("arg", "error"), + [ + (None, InstantError), + ({}, InstantError), + ((), InstantError), + ([], InstantError), + (1, InstantError), + ("", InstantError), + ("à", InstantError), + ("1", InstantError), + ("-1", InstantError), + ("999", InstantError), + ("1000-0", InstantError), + ("1000-1", ParserError), + ("1000-1-1", InstantError), + ("1000-00", InstantError), + ("1000-13", InstantError), + ("1000-01-00", InstantError), + ("1000-01-99", InstantError), + ("2029-02-29", ParserError), + ("1000-W0", InstantError), + ("1000-W1", InstantError), + ("1000-W99", InstantError), + ("1000-W1-0", InstantError), + ("1000-W1-1", InstantError), + ("1000-W1-99", InstantError), + ("1000-W01-0", InstantError), + ("1000-W01-00", InstantError), + ], +) +def test_parse_instant_with_invalid_argument(arg, error) -> None: + with pytest.raises(error): + _parsers.parse_instant(arg) @pytest.mark.parametrize( @@ -15,41 +76,41 @@ ("1001-W01-1", Period((DateUnit.WEEKDAY, Instant((1000, 12, 29)), 1))), ], ) -def test__parse_period(arg, expected) -> None: +def test_parse_period(arg, expected) -> None: assert _parsers.parse_period(arg) == expected @pytest.mark.parametrize( ("arg", "error"), [ - (None, NotImplementedError), - ({}, NotImplementedError), - ((), NotImplementedError), - ([], NotImplementedError), - (1, NotImplementedError), - ("", AttributeError), - ("à", ParserError), - ("1", ValueError), - ("-1", ValueError), - ("999", ParserError), - ("1000-0", ParserError), + (None, PeriodError), + ({}, PeriodError), + ((), PeriodError), + ([], PeriodError), + (1, PeriodError), + ("", PeriodError), + ("à", PeriodError), + ("1", PeriodError), + ("-1", PeriodError), + ("999", PeriodError), + ("1000-0", PeriodError), ("1000-1", ParserError), - ("1000-1-1", ParserError), - ("1000-00", ParserError), - ("1000-13", ParserError), - ("1000-01-00", ParserError), - ("1000-01-99", ParserError), - ("1000-W0", ParserError), - ("1000-W1", ParserError), - ("1000-W99", ParserError), - ("1000-W1-0", ParserError), - ("1000-W1-1", ParserError), - ("1000-W1-99", ParserError), - ("1000-W01-0", ParserError), - ("1000-W01-00", ParserError), + ("1000-1-1", PeriodError), + ("1000-00", PeriodError), + ("1000-13", PeriodError), + ("1000-01-00", PeriodError), + ("1000-01-99", PeriodError), + ("1000-W0", PeriodError), + ("1000-W1", PeriodError), + ("1000-W99", PeriodError), + ("1000-W1-0", PeriodError), + ("1000-W1-1", PeriodError), + ("1000-W1-99", PeriodError), + ("1000-W01-0", PeriodError), + ("1000-W01-00", PeriodError), ], ) -def test__parse_period_with_invalid_argument(arg, error) -> None: +def test_parse_period_with_invalid_argument(arg, error) -> None: with pytest.raises(error): _parsers.parse_period(arg) @@ -61,8 +122,8 @@ def test__parse_period_with_invalid_argument(arg, error) -> None: ("2022-01", DateUnit.MONTH), ("2022-01-01", DateUnit.DAY), ("2022-W01", DateUnit.WEEK), - ("2022-W01-01", DateUnit.WEEKDAY), + ("2022-W01-1", DateUnit.WEEKDAY), ], ) -def test__parse_unit(arg, expected) -> None: +def test_parse_unit(arg, expected) -> None: assert _parsers.parse_unit(arg) == expected diff --git a/openfisca_core/periods/types.py b/openfisca_core/periods/types.py index fdcd8ff5ae..092509c621 100644 --- a/openfisca_core/periods/types.py +++ b/openfisca_core/periods/types.py @@ -1,21 +1,180 @@ +# TODO(): Properly resolve metaclass types. +# https://github.com/python/mypy/issues/14033 + from collections.abc import Sequence -from openfisca_core.types import DateUnit, Instant, InstantStr, Period, PeriodStr +from openfisca_core.types import DateUnit, Instant, Period + +import re + +#: Matches "2015", "2015-01", "2015-01-01" but not "2015-13", "2015-12-32". +iso_format = re.compile(r"^\d{4}(-(?:0[1-9]|1[0-2])(-(?:0[1-9]|[12]\d|3[01]))?)?$") + +#: Matches "2015", "2015-W01", "2015-W53-1" but not "2015-W54", "2015-W10-8". +iso_calendar = re.compile(r"^\d{4}(-W(0[1-9]|[1-4][0-9]|5[0-3]))?(-[1-7])?$") class _SeqIntMeta(type): - def __instancecheck__(self, arg: Sequence[object]) -> bool: - try: - return bool(arg) and all(isinstance(item, int) for item in arg) - except TypeError: - return False + def __instancecheck__(self, arg: object) -> bool: + return ( + bool(arg) + and isinstance(arg, Sequence) + and all(isinstance(item, int) for item in arg) + ) + + +class SeqInt(list[int], metaclass=_SeqIntMeta): # type: ignore[misc] + """A sequence of integers. + + Examples: + >>> isinstance([1, 2, 3], SeqInt) + True + + >>> isinstance((1, 2, 3), SeqInt) + True + + >>> isinstance({1, 2, 3}, SeqInt) + False + + >>> isinstance([1, 2, "3"], SeqInt) + False + + >>> isinstance(1, SeqInt) + False + + >>> isinstance([], SeqInt) + False + + """ + + +class _InstantStrMeta(type): + def __instancecheck__(self, arg: object) -> bool: + return isinstance(arg, (ISOFormatStr, ISOCalendarStr)) + + +class InstantStr(str, metaclass=_InstantStrMeta): # type: ignore[misc] + """A string representing an instant in string format. + + Examples: + >>> isinstance("2015", InstantStr) + True + + >>> isinstance("2015-01", InstantStr) + True + + >>> isinstance("2015-W01", InstantStr) + True + + >>> isinstance("2015-W01-12", InstantStr) + False + + >>> isinstance("week:2015-W01:3", InstantStr) + False + + """ + + __slots__ = () + + +class _ISOFormatStrMeta(type): + def __instancecheck__(self, arg: object) -> bool: + return isinstance(arg, str) and bool(iso_format.match(arg)) + + +class ISOFormatStr(str, metaclass=_ISOFormatStrMeta): # type: ignore[misc] + """A string representing an instant in ISO format. + + Examples: + >>> isinstance("2015", ISOFormatStr) + True + + >>> isinstance("2015-01", ISOFormatStr) + True + + >>> isinstance("2015-01-01", ISOFormatStr) + True + + >>> isinstance("2015-13", ISOFormatStr) + False + + >>> isinstance("2015-W01", ISOFormatStr) + False + + """ + + __slots__ = () + + +class _ISOCalendarStrMeta(type): + def __instancecheck__(self, arg: object) -> bool: + return isinstance(arg, str) and bool(iso_calendar.match(arg)) + + +class ISOCalendarStr(str, metaclass=_ISOCalendarStrMeta): # type: ignore[misc] + """A string representing an instant in ISO calendar. + + Examples: + >>> isinstance("2015", ISOCalendarStr) + True + + >>> isinstance("2015-W01", ISOCalendarStr) + True + + >>> isinstance("2015-W11-7", ISOCalendarStr) + True + + >>> isinstance("2015-W010", ISOCalendarStr) + False + + >>> isinstance("2015-01", ISOCalendarStr) + False + + """ + + __slots__ = () + + +class _PeriodStrMeta(type): + def __instancecheck__(self, arg: object) -> bool: + return ( + isinstance(arg, str) + and ":" in arg + and isinstance(arg.split(":")[1], InstantStr) + ) + + +class PeriodStr(str, metaclass=_PeriodStrMeta): # type: ignore[misc] + """A string representing a period. + + Examples: + >>> isinstance("year", PeriodStr) + False + + >>> isinstance("2015", PeriodStr) + False + + >>> isinstance("year:2015", PeriodStr) + True + + >>> isinstance("month:2015-01", PeriodStr) + True + + >>> isinstance("weekday:2015-W01-1:365", PeriodStr) + True + + >>> isinstance("2015-W01:1", PeriodStr) + False + """ -class SeqInt(list[int], metaclass=_SeqIntMeta): ... + __slots__ = () __all__ = [ "DateUnit", + "ISOCalendarStr", + "ISOFormatStr", "Instant", "InstantStr", "Period", From e1aa7c79b2001585c81eb4fbc44152e722923427 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 26 Sep 2024 22:36:55 +0200 Subject: [PATCH 156/188] refactor(periods)!: change eternity to int (#1229) Previously, eternal periods and entities were populated with `inf` values, which are floats. This was an exception, already, as the rest of the values are integers. If you store, for example, thousands of instants in a numpy array, just one eternal instant will force the whole array to pass from integer to float. Now, eternal instants are populated with `-1`, and can be produced with `Instant.eternity()` and `Period.eternity()`. Also, they can be checked with `is_eternal`. --- CHANGELOG.md | 11 +++++++++++ openfisca_core/periods/helpers.py | 4 ++-- openfisca_core/periods/instant_.py | 6 +++++- openfisca_core/periods/period_.py | 6 +++++- openfisca_core/periods/tests/helpers/test_period.py | 6 +++--- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d03660ef..af653e1e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ #### Breaking changes +- Changes to `eternity` instants and periods + - Eternity instants are now `` instead of + `` + - Eternity periods are now `, -1))>` + instead of `, inf))>` + - The reason is to avoid mixing data types: `inf` is a float, periods and + instants are integers. Mixed data types make memory optimisations impossible. + - Migration should be straightforward. If you have a test that checks for + `inf`, you should update it to check for `-1` or use the `is_eternal` method. - `periods.instant` no longer returns `None` - Now, it raises `periods.InstantError` @@ -12,6 +21,8 @@ - Introduce `Instant.eternity()` - This behaviour was duplicated across - Now it is encapsulated in a single method +- Introduce `Instant.is_eternal` and `Period.is_eternal` + - These methods check if the instant or period are eternity (`bool`). - Now `periods.instant` parses also ISO calendar strings (weeks) - For instance, `2022-W01` is now a valid input diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 6446c0c839..de64e60fe2 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -145,7 +145,7 @@ def period(value: object) -> t.Period: Period((, Instant((2021, 1, 1)), 1)) >>> period(DateUnit.ETERNITY) - Period((, Instant((1, 1, 1)), 0)) + Period((, Instant((-1, -1, -1)), -1)) >>> period(2021) Period((, Instant((2021, 1, 1)), 1)) @@ -173,7 +173,7 @@ def period(value: object) -> t.Period: one, two, three = 1, 2, 3 # We return an "eternity-period", for example - # ``, 0))>``. + # ``, -1))>``. if str(value).lower() == DateUnit.ETERNITY: return Period.eternity() diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 9d26dc815a..f71dbb3222 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -122,6 +122,10 @@ def month(self) -> int: def year(self) -> int: return self[0] + @property + def is_eternal(self) -> bool: + return self == self.eternity() + def offset(self, offset: str | int, unit: t.DateUnit) -> t.Instant | None: """Increments/decrements the given instant with offset units. @@ -214,7 +218,7 @@ def offset(self, offset: str | int, unit: t.DateUnit) -> t.Instant | None: @classmethod def eternity(cls) -> t.Instant: """Return an eternity instant.""" - return cls((1, 1, 1)) + return cls((-1, -1, -1)) __all__ = ["Instant"] diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 87e8465d37..00e833d861 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -827,6 +827,10 @@ def stop(self) -> t.Instant: raise ValueError + @property + def is_eternal(self) -> bool: + return self == self.eternity() + # Reference periods @property @@ -908,7 +912,7 @@ def first_weekday(self) -> t.Period: @classmethod def eternity(cls) -> t.Period: """Return an eternity period.""" - return cls((DateUnit.ETERNITY, Instant.eternity(), 0)) + return cls((DateUnit.ETERNITY, Instant.eternity(), -1)) __all__ = ["Period"] diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py index 5f5e345bcc..d2d5c6679a 100644 --- a/openfisca_core/periods/tests/helpers/test_period.py +++ b/openfisca_core/periods/tests/helpers/test_period.py @@ -9,11 +9,11 @@ @pytest.mark.parametrize( ("arg", "expected"), [ - ("eternity", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0))), - ("ETERNITY", Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0))), + ("eternity", Period((DateUnit.ETERNITY, Instant((-1, -1, -1)), -1))), + ("ETERNITY", Period((DateUnit.ETERNITY, Instant((-1, -1, -1)), -1))), ( DateUnit.ETERNITY, - Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 0)), + Period((DateUnit.ETERNITY, Instant((-1, -1, -1)), -1)), ), (datetime.date(1, 1, 1), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))), (Instant((1, 1, 1)), Period((DateUnit.DAY, Instant((1, 1, 1)), 1))), From e68657e52c489e7ea4b88141e8f40fcc60998fad Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 26 Sep 2024 22:43:17 +0200 Subject: [PATCH 157/188] chore(version): bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index affb374a34..f8fa39b884 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="OpenFisca-Core", - version="41.6.0", + version="42.0.0", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From db3917b7bf3768606b67b9f3a811d2f013fe4085 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 1 Oct 2024 02:57:16 +0200 Subject: [PATCH 158/188] docs(entities): fix (#1252) --- openfisca_core/commons/__init__.py | 2 +- openfisca_core/commons/formulas.py | 13 ++-- openfisca_core/commons/misc.py | 9 +-- openfisca_core/commons/rates.py | 18 ++--- openfisca_core/entities/_core_entity.py | 65 ++++++++++++++++--- openfisca_core/entities/_description.py | 6 +- openfisca_core/entities/entity.py | 24 ++++++- openfisca_core/entities/group_entity.py | 26 ++++++-- openfisca_core/entities/helpers.py | 36 +++++----- openfisca_core/entities/role.py | 32 ++++----- openfisca_core/entities/tests/test_role.py | 3 +- openfisca_core/periods/helpers.py | 6 +- .../simulations/simulation_builder.py | 8 +-- setup.cfg | 2 +- 14 files changed, 161 insertions(+), 89 deletions(-) diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index 039bd0673f..1a3d065ee1 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -33,7 +33,7 @@ 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: + How they could be used in a future release:: from openfisca_core import commons from openfisca_core.commons import deprecated diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 5232c3ba4b..10f3fb55c2 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -24,11 +24,10 @@ def apply_thresholds( choices: A list of the possible values to choose from. Returns: - :obj:`numpy.ndarray` of :obj:`float`: - A list of the values chosen. + Array[numpy.float32]: A list of the values chosen. Raises: - :exc:`AssertionError`: When the number of ``thresholds`` (t) and the + AssertionError: When the number of ``thresholds`` (t) and the number of choices (c) are not either t == c or t == c - 1. Examples: @@ -68,8 +67,7 @@ def concat( that: Another array to concatenate. Returns: - :obj:`numpy.ndarray` of :obj:`numpy.str_`: - An array with the concatenated values. + Array[numpy.str_]: An array with the concatenated values. Examples: >>> this = ["this", "that"] @@ -101,11 +99,10 @@ def switch( value_by_condition: Values to replace for each condition. Returns: - :obj:`numpy.ndarray`: - An array with the replaced values. + Array: An array with the replaced values. Raises: - :exc:`AssertionError`: When ``value_by_condition`` is empty. + AssertionError: When ``value_by_condition`` is empty. Examples: >>> conditions = numpy.array([1, 1, 1, 2]) diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index 93e50c6d90..ba9687619c 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -50,8 +50,8 @@ def stringify_array(array: None | t.Array[numpy.generic]) -> str: array: An array. Returns: - :obj:`str`: - "None" if the ``array`` is None, the stringified ``array`` otherwise. + str: "None" if the ``array`` is None. + str: The stringified ``array`` otherwise. Examples: >>> import numpy @@ -84,10 +84,11 @@ def eval_expression( """Evaluate a string expression to a numpy array. Args: - expression(str): An expression to evaluate. + expression: An expression to evaluate. Returns: - :obj:`object`: The result of the evaluation. + Array: The result of the evaluation. + str: The expression if it couldn't be evaluated. Examples: >>> eval_expression("1 + 2") diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index 3a1ec661c2..11cb261980 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -25,12 +25,9 @@ def average_rate( trim: The lower and upper bounds of the average rate. Returns: - :obj:`numpy.ndarray` of :obj:`float`: - - The average rate for each target. - - When ``trim`` is provided, values that are out of the provided bounds - are replaced by :obj:`numpy.nan`. + Array[numpy.float32]: The average rate for each target. When ``trim`` + is provided, values that are out of the provided bounds are + replaced by :any:`numpy.nan`. Examples: >>> target = numpy.array([1, 2, 3]) @@ -80,12 +77,9 @@ def marginal_rate( 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`. + Array[numpy.float32]: The marginal rate for each target. When ``trim`` + is provided, values that are out of the provided bounds are replaced by + :any:`numpy.nan`. Examples: >>> target = numpy.array([1, 2, 3]) diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index ca3553379e..f44353e112 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -10,12 +10,21 @@ class _CoreEntity: - """Base class to build entities from.""" + """Base class to build entities from. - #: A key to identify the entity. + Args: + __key: A key to identify the ``_CoreEntity``. + __plural: The ``key`` pluralised. + __label: A summary description. + __doc: A full description. + *__args: Additional arguments. + + """ + + #: A key to identify the ``_CoreEntity``. key: t.EntityKey - #: The ``key``, pluralised. + #: The ``key`` pluralised. plural: t.EntityPlural #: A summary description. @@ -24,10 +33,10 @@ class _CoreEntity: #: A full description. doc: str - #: Whether the entity is a person or not. + #: Whether the ``_CoreEntity`` is a person or not. is_person: ClassVar[bool] - #: A TaxBenefitSystem instance. + #: A ``TaxBenefitSystem`` instance. _tax_benefit_system: None | t.TaxBenefitSystem = None @abc.abstractmethod @@ -44,7 +53,7 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key})" def set_tax_benefit_system(self, tax_benefit_system: t.TaxBenefitSystem) -> None: - """An Entity belongs to a TaxBenefitSystem.""" + """A ``_CoreEntity`` belongs to a ``TaxBenefitSystem``.""" self._tax_benefit_system = tax_benefit_system def get_variable( @@ -52,7 +61,22 @@ def get_variable( variable_name: t.VariableName, check_existence: bool = False, ) -> t.Variable | None: - """Get a ``variable_name`` from ``variables``.""" + """Get ``variable_name`` from ``variables``. + + Args: + variable_name: The ``Variable`` to be found. + check_existence: Was the ``Variable`` found? + + Returns: + Variable: When the ``Variable`` exists. + None: When the ``Variable`` doesn't exist. + + Raises: + ValueError: When ``check_existence`` is ``True`` and + the ``Variable`` doesn't exist. + + """ + if self._tax_benefit_system is None: msg = "You must set 'tax_benefit_system' before calling this method." raise ValueError( @@ -61,7 +85,21 @@ def get_variable( return self._tax_benefit_system.get_variable(variable_name, check_existence) def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> None: - """Check if ``variable_name`` is defined for ``self``.""" + """Check if ``variable_name`` is defined for ``self``. + + Args: + variable_name: The ``Variable`` to be found. + + Returns: + Variable: When the ``Variable`` exists. + None: When the :attr:`_tax_benefit_system` is not set. + + Raises: + ValueError: When the ``Variable`` exists but is defined + for another ``Entity``. + + """ + entity: None | t.CoreEntity = None variable: None | t.Variable = self.get_variable( variable_name, @@ -86,7 +124,16 @@ def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> No @staticmethod def check_role_validity(role: object) -> None: - """Check if a ``role`` is an instance of Role.""" + """Check if ``role`` is an instance of ``Role``. + + Args: + role: Any object. + + Raises: + ValueError: When ``role`` is not a ``Role``. + + """ + if role is not None and not isinstance(role, Role): msg = f"{role} is not a valid role" raise ValueError(msg) diff --git a/openfisca_core/entities/_description.py b/openfisca_core/entities/_description.py index 6e0a62beb8..78634ca270 100644 --- a/openfisca_core/entities/_description.py +++ b/openfisca_core/entities/_description.py @@ -6,7 +6,7 @@ @dataclasses.dataclass(frozen=True) class _Description: - r"""A Role's description. + """A ``Role``'s description. Examples: >>> data = { @@ -35,10 +35,10 @@ class _Description: """ - #: A key to identify the Role. + #: A key to identify the ``Role``. key: str - #: The ``key``, pluralised. + #: The ``key`` pluralised. plural: None | str = None #: A summary description. diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 3b1a6713e8..c001918168 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -7,9 +7,29 @@ class Entity(_CoreEntity): - """An entity (e.g. a person, a household) on which calculations can be run.""" + """An entity (e.g. a person, a household) on which calculations can be run. - #: Whether the entity is a person or not. + Args: + key: A key to identify the ``Entity``. + plural: The ``key`` pluralised. + label: A summary description. + doc: A full description. + + """ + + #: A key to identify the ``Entity``. + key: t.EntityKey + + #: The ``key`` pluralised. + plural: t.EntityPlural + + #: A summary description. + label: str + + #: A full description. + doc: str + + #: Whether the ``Entity`` is a person or not. is_person: ClassVar[bool] = True def __init__(self, key: str, plural: str, label: str, doc: str) -> None: diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index 5ae74de560..4b588567ad 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -14,21 +14,35 @@ class GroupEntity(_CoreEntity): """Represents an entity containing several others with different roles. - A :class:`.GroupEntity` represents an :class:`.Entity` containing - several other :class:`.Entity` with different :class:`.Role`, and on - which calculations can be run. + A ``GroupEntity`` represents an ``Entity`` containing several other entities, + with different roles, and on which calculations can be run. Args: - key: A key to identify the group entity. - plural: The ``key``, pluralised. + key: A key to identify the ``GroupEntity``. + plural: The ``key`` pluralised. label: A summary description. doc: A full description. - roles: The list of :class:`.Role` of the group entity. + roles: The list of roles of the group entity. containing_entities: The list of keys of group entities whose members are guaranteed to be a superset of this group's entities. """ + #: A key to identify the ``Entity``. + key: t.EntityKey + + #: The ``key`` pluralised. + plural: t.EntityPlural + + #: A summary description. + label: str + + #: A full description. + doc: str + + #: The list of roles of the ``GroupEntity``. + roles: Iterable[Role] + #: Whether the entity is a person or not. is_person: ClassVar[bool] = False diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 808ff0e61a..146ab6d25b 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -14,39 +14,40 @@ def build_entity( doc: str = "", roles: None | Sequence[t.RoleParams] = None, is_person: bool = False, + *, class_override: object = None, containing_entities: Sequence[str] = (), ) -> t.SingleEntity | t.GroupEntity: - """Build a SingleEntity or a GroupEntity. + """Build an ``Entity`` or a ``GroupEntity``. Args: - key: Key to identify the :class:`.Entity`. or :class:`.GroupEntity`. - plural: ``key``, pluralised. + key: Key to identify the ``Entity`` or ``GroupEntity``. + plural: The ``key`` pluralised. label: A summary description. doc: A full description. - roles: A list of :class:`.Role`, if it's a :class:`.GroupEntity`. + roles: A list of roles —if it's a ``GroupEntity``. is_person: If is an individual, or not. class_override: ? containing_entities: Keys of contained entities. Returns: - :obj:`.Entity` or :obj:`.GroupEntity`: - :obj:`.Entity`: When ``is_person`` is True. - :obj:`.GroupEntity`: When ``is_person`` is False. + Entity: When ``is_person`` is ``True``. + GroupEntity: When ``is_person`` is ``False``. Raises: - NotImplementedError: if ``roles`` is None. + NotImplementedError: If ``roles`` is ``None``. Examples: >>> from openfisca_core import entities - >>> build_entity( + >>> entity = build_entity( ... "syndicate", ... "syndicates", ... "Banks loaning jointly.", ... roles=[], ... containing_entities=(), ... ) + >>> entity GroupEntity(syndicate) >>> build_entity( @@ -57,16 +58,16 @@ def build_entity( ... ) Entity(company) - >>> role = entities.Role({"key": "key"}, object()) + >>> role = entities.Role({"key": "key"}, entity) >>> build_entity( ... "syndicate", ... "syndicates", ... "Banks loaning jointly.", - ... roles=role, + ... roles=[role], ... ) Traceback (most recent call last): - TypeError: 'Role' object is not iterable + TypeError: 'Role' object is not subscriptable """ @@ -92,15 +93,16 @@ def find_role( *, total: None | int = None, ) -> None | t.Role: - """Find a Role in a GroupEntity. + """Find a ``Role`` in a ``GroupEntity``. Args: - roles (Iterable[Role]): The roles to search. - key (str): The key of the role to find. Defaults to `None`. - total (int | None): The `max` attribute of the role to find. + roles: The roles to search. + key: The key of the role to find. + total: The ``max`` attribute of the role to find. Returns: - Role | None: The role if found, else `None`. + Role: The role if found + None: Else ``None``. Examples: >>> from openfisca_core.entities.types import RoleParams diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 0cb83cde53..f97fe64033 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -7,25 +7,21 @@ class Role: - """The role of an Entity within a GroupEntity. + """The role of an ``Entity`` within a ``GroupEntity``. - Each Entity related to a GroupEntity has a Role. For example, if you have - a family, its roles could include a parent, a child, and so on. Or if you - have a tax household, its roles could include the taxpayer, a spouse, - several dependents, and the like. - - Attributes: - entity (Entity): The Entity the Role belongs to. - description (_Description): A description of the Role. - max (int): Max number of members. - subroles (list[Role]): A list of subroles. + Each ``Entity`` related to a ``GroupEntity`` has a ``Role``. For example, + if you have a family, its roles could include a parent, a child, and so on. + Or if you have a tax household, its roles could include thetaxpayer, a + spouse, several dependents, and the like. Args: - description (dict): A description of the Role. - entity (Entity): The Entity to which the Role belongs. + description: A description of the Role. + entity: The Entity to which the Role belongs. Examples: - >>> role = Role({"key": "parent"}, object()) + >>> from openfisca_core import entities + >>> entity = entities.GroupEntity("key", "plural", "label", "doc", []) + >>> role = entities.Role({"key": "parent"}, entity) >>> repr(Role) "" @@ -44,10 +40,10 @@ class Role: """ - #: The Entity the Role belongs to. + #: The ``GroupEntity`` the Role belongs to. entity: t.GroupEntity - #: A description of the Role. + #: A description of the ``Role``. description: _Description #: Max number of members. @@ -58,12 +54,12 @@ class Role: @property def key(self) -> t.RoleKey: - """A key to identify the Role.""" + """A key to identify the ``Role``.""" return t.RoleKey(self.description.key) @property def plural(self) -> None | t.RolePlural: - """The ``key``, pluralised.""" + """The ``key`` pluralised.""" if (plural := self.description.plural) is None: return None return t.RolePlural(plural) diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py index ffb1fdddb8..454d862c70 100644 --- a/openfisca_core/entities/tests/test_role.py +++ b/openfisca_core/entities/tests/test_role.py @@ -5,6 +5,7 @@ def test_init_when_doc_indented() -> None: """De-indent the ``doc`` attribute if it is passed at initialisation.""" key = "\tkey" doc = "\tdoc" - role = entities.Role({"key": key, "doc": doc}, object()) + entity = entities.GroupEntity("key", "plural", "label", "doc", []) + role = entities.Role({"key": key, "doc": doc}, entity) assert role.key == key assert role.doc == doc.lstrip() diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index de64e60fe2..fab26c48ab 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -99,14 +99,14 @@ def _(value: str) -> t.Instant: def instant_date(instant: None | t.Instant) -> None | datetime.date: - """Returns the date representation of an :class:`.Instant`. + """Returns the date representation of an ``Instant``. Args: - instant (:obj:`.Instant`, optional): + instant: An ``Instant``. Returns: None: When ``instant`` is None. - :obj:`datetime.date`: Otherwise. + datetime.date: Otherwise. Examples: >>> instant_date(Instant((2021, 1, 1))) diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 96d451cd78..064b5b4cb6 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -82,8 +82,8 @@ def build_from_dict( :meth:`.SimulationBuilder.build_from_variables` if they are not. Args: - tax_benefit_system(TaxBenefitSystem): The system to use. - input_dict(Params): The input of the simulation. + tax_benefit_system: The system to use. + input_dict: The input of the simulation. Returns: Simulation: The built simulation. @@ -281,8 +281,8 @@ def build_from_variables( infer an entity structure. Args: - tax_benefit_system(TaxBenefitSystem): The system to use. - input_dict(Variables): The input of the simulation. + tax_benefit_system: The system to use. + input_dict: The input of the simulation. Returns: Simulation: The built simulation. diff --git a/setup.cfg b/setup.cfg index c31d9f9e7e..9b8ce699bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,7 @@ convention = google docstring_style = google extend-ignore = D -ignore = B019, E203, E501, F405, E701, E704, RST212, RST301, W503 +ignore = B019, E203, E501, F405, E701, E704, RST212, RST213, RST301, RST306, W503 in-place = true include-in-doctest = openfisca_core/commons From 9bf60a828d15f396b4eca10e53d15f21a1859a50 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga-Alvarado Date: Tue, 1 Oct 2024 11:57:53 +0000 Subject: [PATCH 159/188] docs(entities): fix typo (#1252) Co-authored-by: Mahdi Ben Jelloul --- openfisca_core/entities/role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index f97fe64033..e687b2604c 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -11,7 +11,7 @@ class Role: Each ``Entity`` related to a ``GroupEntity`` has a ``Role``. For example, if you have a family, its roles could include a parent, a child, and so on. - Or if you have a tax household, its roles could include thetaxpayer, a + Or if you have a tax household, its roles could include the taxpayer, a spouse, several dependents, and the like. Args: From d94bf4d1c4980aee11de9b6cb7b47da93a202b64 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 1 Oct 2024 03:01:22 +0200 Subject: [PATCH 160/188] chore: version bump --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af653e1e65..e053bcb45c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 42.0.1 [#1253](https://github.com/openfisca/openfisca-core/pull/1253) + +#### Documentation + +- Fix documentation of `entities` + # 42.0.0 [#1223](https://github.com/openfisca/openfisca-core/pull/1223) #### Breaking changes diff --git a/setup.py b/setup.py index f8fa39b884..5f838e993e 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="OpenFisca-Core", - version="42.0.0", + version="42.0.1", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 39af3c2fc65293cce2079dae56054d3be656edc0 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 1 Oct 2024 14:22:26 +0200 Subject: [PATCH 161/188] docs(commons): fix bad indent --- openfisca_core/commons/formulas.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 10f3fb55c2..bf1ba67c04 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -27,8 +27,7 @@ def apply_thresholds( Array[numpy.float32]: A list of the values chosen. Raises: - AssertionError: When the number of ``thresholds`` (t) and the - number of choices (c) are not either t == c or t == c - 1. + AssertionError: When thresholds and choices are incompatible. Examples: >>> input = numpy.array([4, 5, 6, 7, 8]) From a8869971177d14a6bb66f1e19fd54e6dc3c7372b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 1 Oct 2024 14:24:12 +0200 Subject: [PATCH 162/188] chore: version bump --- CHANGELOG.md | 6 ++++++ setup.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e053bcb45c..49b914428e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 42.0.2 [#1256](https://github.com/openfisca/openfisca-core/pull/1256) + +#### Documentation + +- Fix bad indent + ### 42.0.1 [#1253](https://github.com/openfisca/openfisca-core/pull/1253) #### Documentation diff --git a/setup.py b/setup.py index 5f838e993e..0c021be25c 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="OpenFisca-Core", - version="42.0.1", + version="42.0.2", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 4354256529c0765c01ca1e47ec526af3d5720f04 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 27 Sep 2024 17:31:23 +0200 Subject: [PATCH 163/188] ci: add test matrix --- .github/publish-git-tag.sh | 4 - .github/workflows/_before.yaml | 102 +++++++++++ .github/workflows/_lint.yaml | 57 ++++++ .github/workflows/_test.yaml | 72 ++++++++ .github/workflows/merge.yaml | 242 +++++++++++++++++++++++++ .github/workflows/push.yaml | 84 +++++++++ .github/workflows/workflow.yml | 312 --------------------------------- openfisca_tasks/publish.mk | 8 + 8 files changed, 565 insertions(+), 316 deletions(-) delete mode 100755 .github/publish-git-tag.sh create mode 100644 .github/workflows/_before.yaml create mode 100644 .github/workflows/_lint.yaml create mode 100644 .github/workflows/_test.yaml create mode 100644 .github/workflows/merge.yaml create mode 100644 .github/workflows/push.yaml delete mode 100644 .github/workflows/workflow.yml diff --git a/.github/publish-git-tag.sh b/.github/publish-git-tag.sh deleted file mode 100755 index 4450357cbc..0000000000 --- a/.github/publish-git-tag.sh +++ /dev/null @@ -1,4 +0,0 @@ -#! /usr/bin/env bash - -git tag `python setup.py --version` -git push --tags # update the repository version diff --git a/.github/workflows/_before.yaml b/.github/workflows/_before.yaml new file mode 100644 index 0000000000..d347057bb3 --- /dev/null +++ b/.github/workflows/_before.yaml @@ -0,0 +1,102 @@ +name: Setup package + +on: + workflow_call: + inputs: + os: + required: true + type: string + + numpy: + required: true + type: string + + python: + required: true + type: string + + activate_command: + required: true + type: string + +jobs: + deps: + runs-on: ${{ inputs.os }} + name: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + env: + TERM: xterm-256color # To colorize output of make tasks. + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + id: restore-deps + uses: actions/cache@v3 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + restore-keys: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- + + - name: Install dependencies + run: | + python -m venv venv + ${{ inputs.activate_command }} + make install-deps install-dist + + build: + runs-on: ${{ inputs.os }} + needs: [ deps ] + name: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + env: + TERM: xterm-256color # To colorize output of make tasks. + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + + - name: Cache build + uses: actions/cache@v3 + with: + path: venv/**/[Oo]pen[Ff]isca* + key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + restore-keys: | + build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- + build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- + + - name: Cache release + uses: actions/cache@v3 + with: + path: dist + key: release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Build package + run: | + ${{ inputs.activate_command }} + make install-test clean build diff --git a/.github/workflows/_lint.yaml b/.github/workflows/_lint.yaml new file mode 100644 index 0000000000..6f34c02b36 --- /dev/null +++ b/.github/workflows/_lint.yaml @@ -0,0 +1,57 @@ +name: Lint package + +on: + workflow_call: + inputs: + os: + required: true + type: string + + numpy: + required: true + type: string + + python: + required: true + type: string + + activate_command: + required: true + type: string + +jobs: + lint: + runs-on: ${{ inputs.os }} + name: lint-doc-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + env: + TERM: xterm-256color # To colorize output of make tasks. + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + + - name: Lint doc + run: | + ${{ inputs.activate_command }} + make clean check-syntax-errors lint-doc + + - name: Lint styles + run: | + ${{ inputs.activate_command }} + make clean check-syntax-errors check-style diff --git a/.github/workflows/_test.yaml b/.github/workflows/_test.yaml new file mode 100644 index 0000000000..eda7224bb7 --- /dev/null +++ b/.github/workflows/_test.yaml @@ -0,0 +1,72 @@ +name: Test package + +on: + workflow_call: + inputs: + os: + required: true + type: string + + numpy: + required: true + type: string + + python: + required: true + type: string + + activate_command: + required: true + type: string + +jobs: + test: + runs-on: ${{ inputs.os }} + name: test-core-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TERM: xterm-256color # To colorize output of make tasks. + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + + - name: Cache build + uses: actions/cache@v3 + with: + path: venv/**/[Oo]pen[Ff]isca* + key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Run Openfisca Core tests + run: | + ${{ inputs.activate_command }} + make test-core + python -m coveralls --service=github + + - name: Run Country Template tests + if: ${{ startsWith(inputs.os, 'ubuntu') }} + run: | + ${{ inputs.activate_command }} + make test-country + + - name: Run Extension Template tests + if: ${{ startsWith(inputs.os, 'ubuntu') }} + run: | + ${{ inputs.activate_command }} + make test-extension diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml new file mode 100644 index 0000000000..86bbc87df1 --- /dev/null +++ b/.github/workflows/merge.yaml @@ -0,0 +1,242 @@ +name: OpenFisca-Core / Deploy package to PyPi & Conda + +on: + push: + branches: [master] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + setup: + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, windows-latest] + numpy: [2.1.1, 1.24.2] + python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + include: + - os: ubuntu-latest + activate_command: source venv/bin/activate + - os: windows-latest + activate_command: .\venv\Scripts\activate + uses: ./.github/workflows/_before.yaml + with: + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} + + test: + needs: [setup] + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, windows-latest] + numpy: [2.1.1, 1.24.2] + python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + include: + - os: ubuntu-latest + activate_command: source venv/bin/activate + - os: windows-latest + activate_command: .\venv\Scripts\activate + uses: ./.github/workflows/_test.yaml + with: + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} + + lint: + needs: [setup] + strategy: + fail-fast: true + matrix: + numpy: [1.24.2] + python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + uses: ./.github/workflows/_lint.yaml + with: + os: ubuntu-latest + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: source venv/bin/activate + + # The idea behind these dependencies is we want to give feedback to + # contributors on the version number only after they have passed all tests, + # so they don't have to do it twice after changes happened to the main branch + # during the time they took to fix the tests. + check-version: + runs-on: ubuntu-latest + needs: [test, lint] + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9.20 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + + - name: Check version number has been properly updated + run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" + + # GitHub Actions does not have a halt job option, to stop from deploying if + # no functional changes were found. We build a separate job to substitute the + # halt option. The `deploy` job is dependent on the output of the + # `check-for-functional-changes`job. + check-for-functional-changes: + runs-on: ubuntu-latest + needs: [check-version] # Last job to run + outputs: + status: ${{ steps.stop-early.outputs.status }} + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9.20 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + + - id: stop-early + run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi # The `check-for-functional-changes` job should always succeed regardless of the `has-functional-changes` script's exit code. Consequently, we do not use that exit code to trigger deploy, but rather a dedicated output variable `status`, to avoid a job failure if the exit code is different from 0. Conversely, if the job fails the entire workflow would be marked as `failed` which is disturbing for contributors. + + publish-to-pypi: + runs-on: ubuntu-20.04 + needs: [check-for-functional-changes] + if: needs.check-for-functional-changes.outputs.status == 'success' + env: + PYPI_USERNAME: __token__ + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_OPENFISCA_BOT }} + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9.20 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + + - name: Cache deps + uses: actions/cache@v3 + with: + path: venv + key: deps-ubuntu-20.04-np1.20.3-py3.8.10-${{ hashFiles('setup.py') }} + + - name: Cache build + uses: actions/cache@v3 + with: + path: venv/**/[oO]pen[fF]isca* + key: build-ubuntu-20.04-np1.20.3-py3.8.10-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Cache release + uses: actions/cache@v3 + with: + path: dist + key: release-ubuntu-20.04-np1.20.3-py3.8.10-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Upload package to PyPi + run: | + source venv/bin/activate + make publish + + - name: Update doc + run: | + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ + -d '{"ref":"master"}' + + build-conda: + runs-on: ubuntu-22.04 + needs: [setup] + # Do not build on master, the artifact will be used + if: github.ref != 'refs/heads/master' + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: "3.10.6" + # Add conda-forge for OpenFisca-Core + channels: conda-forge + activate-environment: true + - uses: actions/checkout@v3 + - name: Display version + run: echo "version=`python setup.py --version`" + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + - name: Build Conda package + run: conda build --croot /tmp/conda .conda + - name: Upload Conda build + uses: actions/upload-artifact@v3 + with: + name: conda-build-`python setup.py --version`-${{ github.sha }} + path: /tmp/conda + + publish-to-conda: + runs-on: ubuntu-22.04 + needs: [publish-to-pypi] + strategy: + fail-fast: false + + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: 3.10.6 + channels: conda-forge + activate-environment: true + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Update meta.yaml + run: | + python3 -m pip install requests argparse + # Sleep to allow PyPi to update its API + sleep 60 + python3 .github/get_pypi_info.py -p OpenFisca-Core + + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + conda config --set anaconda_upload yes + + - name: Conda build + run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user openfisca .conda + + test-on-windows: + runs-on: windows-latest + needs: [publish-to-conda] + + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: "3.10.6" # See GHA Windows https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json + channels: conda-forge + activate-environment: true + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Install with conda + run: conda install -c openfisca openfisca-core + + - name: Test openfisca + run: openfisca --help diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml new file mode 100644 index 0000000000..73b6b75079 --- /dev/null +++ b/.github/workflows/push.yaml @@ -0,0 +1,84 @@ +name: OpenFisca-Core / Pull request review + +on: + pull_request: + types: [assigned, opened, reopened, synchronize, ready_for_review] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + setup: + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, windows-latest] + numpy: [2.1.1, 1.24.2] + python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + include: + - os: ubuntu-latest + activate_command: source venv/bin/activate + - os: windows-latest + activate_command: .\venv\Scripts\activate + uses: ./.github/workflows/_before.yaml + with: + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} + + test: + needs: [setup] + strategy: + fail-fast: true + matrix: + os: [ubuntu-latest, windows-latest] + numpy: [2.1.1, 1.24.2] + python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + include: + - os: ubuntu-latest + activate_command: source venv/bin/activate + - os: windows-latest + activate_command: .\venv\Scripts\activate + uses: ./.github/workflows/_test.yaml + with: + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} + + lint: + needs: [setup] + strategy: + fail-fast: true + matrix: + numpy: [1.24.2] + python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + uses: ./.github/workflows/_lint.yaml + with: + os: ubuntu-latest + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: source venv/bin/activate + + # The idea behind these dependencies is we want to give feedback to + # contributors on the version number only after they have passed all tests, + # so they don't have to do it twice after changes happened to the main branch + # during the time they took to fix the tests. + check-version: + runs-on: ubuntu-latest + needs: [test, lint] + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9.20 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + + - name: Check version number has been properly updated + run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml deleted file mode 100644 index 727a25e8c7..0000000000 --- a/.github/workflows/workflow.yml +++ /dev/null @@ -1,312 +0,0 @@ -name: OpenFisca-Core - -on: [ push, pull_request, workflow_dispatch ] - -jobs: - build: - runs-on: ubuntu-22.04 - env: - TERM: xterm-256color # To colorize output of make tasks. - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - restore-keys: | # in case of a cache miss (systematically unless the same commit is built repeatedly), the keys below will be used to restore dependencies from previous builds, and the cache will be stored at the end of the job, making up-to-date dependencies available for all jobs of the workflow; see more at https://docs.github.com/en/actions/advanced-guides/caching-dependencies-to-speed-up-workflows#example-using-the-cache-action - build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }} - build-${{ env.pythonLocation }}- - - - name: Build package - run: make install-deps install-dist install-test clean build - - - name: Cache release - id: restore-release - uses: actions/cache@v2 - with: - path: dist - key: release-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - test-core: - runs-on: ubuntu-22.04 - needs: [ build ] - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TERM: xterm-256color # To colorize output of make tasks. - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Run openfisca-core tests - run: make test-core - - - name: Submit coverage to Coveralls - run: coveralls --service=github - - test-country-template: - runs-on: ubuntu-22.04 - needs: [ build ] - env: - TERM: xterm-256color - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Run Country Template tests - run: make test-country - - test-extension-template: - runs-on: ubuntu-22.04 - needs: [ build ] - env: - TERM: xterm-256color - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Run Extension Template tests - run: make test-extension - - lint-files: - runs-on: ubuntu-22.04 - needs: [ build ] - env: - TERM: xterm-256color - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Run linters - run: make lint - - check-version: - runs-on: ubuntu-22.04 - needs: [ test-core, test-country-template, test-extension-template, lint-files ] # Last job to run - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Check version number has been properly updated - run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" - - # GitHub Actions does not have a halt job option, to stop from deploying if no functional changes were found. - # We build a separate job to substitute the halt option. - # The `deploy` job is dependent on the output of the `check-for-functional-changes`job. - check-for-functional-changes: - runs-on: ubuntu-22.04 - if: github.ref == 'refs/heads/master' # Only triggered for the `master` branch - needs: [ check-version ] - outputs: - status: ${{ steps.stop-early.outputs.status }} - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - id: stop-early - run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi # The `check-for-functional-changes` job should always succeed regardless of the `has-functional-changes` script's exit code. Consequently, we do not use that exit code to trigger deploy, but rather a dedicated output variable `status`, to avoid a job failure if the exit code is different from 0. Conversely, if the job fails the entire workflow would be marked as `failed` which is disturbing for contributors. - - deploy: - runs-on: ubuntu-22.04 - needs: [ check-for-functional-changes ] - if: needs.check-for-functional-changes.outputs.status == 'success' - env: - PYPI_USERNAME: __token__ - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_OPENFISCA_BOT }} - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Cache release - id: restore-release - uses: actions/cache@v2 - with: - path: dist - key: release-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Upload a Python package to PyPi - run: twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN - - - name: Publish a git tag - run: "${GITHUB_WORKSPACE}/.github/publish-git-tag.sh" - - - name: Update doc - run: | - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ - -d '{"ref":"master"}' - - build-conda: - runs-on: ubuntu-22.04 - needs: [ build ] - # Do not build on master, the artifact will be used - if: github.ref != 'refs/heads/master' - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: "3.10.6" - # Add conda-forge for OpenFisca-Core - channels: conda-forge - activate-environment: true - - uses: actions/checkout@v3 - - name: Display version - run: echo "version=`python setup.py --version`" - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - - name: Build Conda package - run: conda build --croot /tmp/conda .conda - - name: Upload Conda build - uses: actions/upload-artifact@v3 - with: - name: conda-build-`python setup.py --version`-${{ github.sha }} - path: /tmp/conda - - publish-to-conda: - runs-on: "ubuntu-22.04" - needs: [ deploy ] - strategy: - fail-fast: false - - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: 3.10.6 - channels: conda-forge - activate-environment: true - - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Update meta.yaml - run: | - python3 -m pip install requests argparse - # Sleep to allow PyPi to update its API - sleep 60 - python3 .github/get_pypi_info.py -p OpenFisca-Core - - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - conda config --set anaconda_upload yes - - - name: Conda build - run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user openfisca .conda - - test-on-windows: - runs-on: "windows-latest" - needs: [ publish-to-conda ] - - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: "3.10.6" # See GHA Windows https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json - channels: conda-forge - activate-environment: true - - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Install with conda - run: conda install -c openfisca openfisca-core - - - name: Test openfisca - run: openfisca --help diff --git a/openfisca_tasks/publish.mk b/openfisca_tasks/publish.mk index aeeb51141b..802c911fc8 100644 --- a/openfisca_tasks/publish.mk +++ b/openfisca_tasks/publish.mk @@ -15,3 +15,11 @@ build: @pip uninstall --yes openfisca-core @find dist -name "*.whl" -exec pip install --no-deps {} \; @$(call print_pass,$@:) + +## Upload to PyPi. +publish: + @$(call print_help,$@:) + @twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN + @git tag `python setup.py --version` + @git push --tags # update the repository version + @$(call print_pass,$@:) From 21d2fab3a820683653c6112134e4f3adb331cc22 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 27 Sep 2024 17:37:17 +0200 Subject: [PATCH 164/188] ci: fix py version for win --- .github/workflows/merge.yaml | 6 +++--- .github/workflows/push.yaml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 86bbc87df1..88e6a984c6 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -15,7 +15,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] numpy: [2.1.1, 1.24.2] - python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - os: ubuntu-latest activate_command: source venv/bin/activate @@ -35,7 +35,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] numpy: [2.1.1, 1.24.2] - python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - os: ubuntu-latest activate_command: source venv/bin/activate @@ -54,7 +54,7 @@ jobs: fail-fast: true matrix: numpy: [1.24.2] - python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. uses: ./.github/workflows/_lint.yaml with: os: ubuntu-latest diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 73b6b75079..29d4b3c3cd 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -15,7 +15,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] numpy: [2.1.1, 1.24.2] - python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - os: ubuntu-latest activate_command: source venv/bin/activate @@ -35,7 +35,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] numpy: [2.1.1, 1.24.2] - python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - os: ubuntu-latest activate_command: source venv/bin/activate @@ -54,7 +54,7 @@ jobs: fail-fast: true matrix: numpy: [1.24.2] - python: [3.12.6, 3.9.20] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. uses: ./.github/workflows/_lint.yaml with: os: ubuntu-latest From 102c52e5ca4fddafd479909600afb82ed8e6b720 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 27 Sep 2024 17:55:51 +0200 Subject: [PATCH 165/188] ci: fix py version for numpy --- .github/workflows/merge.yaml | 6 +++--- .github/workflows/push.yaml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 88e6a984c6..9abe151764 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -15,7 +15,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] numpy: [2.1.1, 1.24.2] - python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - os: ubuntu-latest activate_command: source venv/bin/activate @@ -35,7 +35,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] numpy: [2.1.1, 1.24.2] - python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - os: ubuntu-latest activate_command: source venv/bin/activate @@ -54,7 +54,7 @@ jobs: fail-fast: true matrix: numpy: [1.24.2] - python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. uses: ./.github/workflows/_lint.yaml with: os: ubuntu-latest diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 29d4b3c3cd..9991fe1e12 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -15,7 +15,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] numpy: [2.1.1, 1.24.2] - python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - os: ubuntu-latest activate_command: source venv/bin/activate @@ -35,7 +35,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest] numpy: [2.1.1, 1.24.2] - python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - os: ubuntu-latest activate_command: source venv/bin/activate @@ -54,7 +54,7 @@ jobs: fail-fast: true matrix: numpy: [1.24.2] - python: [3.12.6, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. uses: ./.github/workflows/_lint.yaml with: os: ubuntu-latest From 62335a7b8bc35f38e94144e2ee9f30274d43eca2 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 27 Sep 2024 18:10:44 +0200 Subject: [PATCH 166/188] ci: do not latest os versions --- .github/workflows/merge.yaml | 34 +++++++++++++++++----------------- .github/workflows/push.yaml | 18 +++++++++--------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 9abe151764..d52f83daa0 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -13,13 +13,13 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-22.04, windows-2019] numpy: [2.1.1, 1.24.2] python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - - os: ubuntu-latest + - os: ubuntu-22.04 activate_command: source venv/bin/activate - - os: windows-latest + - os: windows-2019 activate_command: .\venv\Scripts\activate uses: ./.github/workflows/_before.yaml with: @@ -33,13 +33,13 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-22.04, windows-2019] numpy: [2.1.1, 1.24.2] python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - - os: ubuntu-latest + - os: ubuntu-22.04 activate_command: source venv/bin/activate - - os: windows-latest + - os: windows-2019 activate_command: .\venv\Scripts\activate uses: ./.github/workflows/_test.yaml with: @@ -57,7 +57,7 @@ jobs: python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. uses: ./.github/workflows/_lint.yaml with: - os: ubuntu-latest + os: ubuntu-22.04 numpy: ${{ matrix.numpy }} python: ${{ matrix.python }} activate_command: source venv/bin/activate @@ -67,7 +67,7 @@ jobs: # so they don't have to do it twice after changes happened to the main branch # during the time they took to fix the tests. check-version: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: [test, lint] steps: @@ -78,7 +78,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.9.20 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python-version: 3.9.13 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. - name: Check version number has been properly updated run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" @@ -88,7 +88,7 @@ jobs: # halt option. The `deploy` job is dependent on the output of the # `check-for-functional-changes`job. check-for-functional-changes: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: [check-version] # Last job to run outputs: status: ${{ steps.stop-early.outputs.status }} @@ -101,13 +101,13 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.9.20 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python-version: 3.9.13 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. - id: stop-early run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi # The `check-for-functional-changes` job should always succeed regardless of the `has-functional-changes` script's exit code. Consequently, we do not use that exit code to trigger deploy, but rather a dedicated output variable `status`, to avoid a job failure if the exit code is different from 0. Conversely, if the job fails the entire workflow would be marked as `failed` which is disturbing for contributors. publish-to-pypi: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 needs: [check-for-functional-changes] if: needs.check-for-functional-changes.outputs.status == 'success' env: @@ -122,25 +122,25 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.9.20 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python-version: 3.9.13 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. - name: Cache deps uses: actions/cache@v3 with: path: venv - key: deps-ubuntu-20.04-np1.20.3-py3.8.10-${{ hashFiles('setup.py') }} + key: deps-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }} - name: Cache build uses: actions/cache@v3 with: path: venv/**/[oO]pen[fF]isca* - key: build-ubuntu-20.04-np1.20.3-py3.8.10-${{ hashFiles('setup.py') }}-${{ github.sha }} + key: build-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ github.sha }} - name: Cache release uses: actions/cache@v3 with: path: dist - key: release-ubuntu-20.04-np1.20.3-py3.8.10-${{ hashFiles('setup.py') }}-${{ github.sha }} + key: release-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ github.sha }} - name: Upload package to PyPi run: | @@ -220,7 +220,7 @@ jobs: run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user openfisca .conda test-on-windows: - runs-on: windows-latest + runs-on: windows-2019 needs: [publish-to-conda] steps: diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 9991fe1e12..1782500c8a 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -13,13 +13,13 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-22.04, windows-2019] numpy: [2.1.1, 1.24.2] python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - - os: ubuntu-latest + - os: ubuntu-22.04 activate_command: source venv/bin/activate - - os: windows-latest + - os: windows-2019 activate_command: .\venv\Scripts\activate uses: ./.github/workflows/_before.yaml with: @@ -33,13 +33,13 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-22.04, windows-2019] numpy: [2.1.1, 1.24.2] python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. include: - - os: ubuntu-latest + - os: ubuntu-22.04 activate_command: source venv/bin/activate - - os: windows-latest + - os: windows-2019 activate_command: .\venv\Scripts\activate uses: ./.github/workflows/_test.yaml with: @@ -57,7 +57,7 @@ jobs: python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. uses: ./.github/workflows/_lint.yaml with: - os: ubuntu-latest + os: ubuntu-22.04 numpy: ${{ matrix.numpy }} python: ${{ matrix.python }} activate_command: source venv/bin/activate @@ -67,7 +67,7 @@ jobs: # so they don't have to do it twice after changes happened to the main branch # during the time they took to fix the tests. check-version: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 needs: [test, lint] steps: @@ -78,7 +78,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.9.20 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python-version: 3.9.13 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. - name: Check version number has been properly updated run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" From aad4ff0542f86f109725ea5cbde051d8e4bcd7a0 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 27 Sep 2024 18:22:05 +0200 Subject: [PATCH 167/188] build(make): adapt to win --- openfisca_tasks/install.mk | 7 ++++--- openfisca_tasks/lint.mk | 16 ++++++++-------- openfisca_tasks/publish.mk | 8 ++++---- openfisca_tasks/test_code.mk | 29 ++++++++++++++++++----------- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/openfisca_tasks/install.mk b/openfisca_tasks/install.mk index 0a8c81115b..bb844b9d56 100644 --- a/openfisca_tasks/install.mk +++ b/openfisca_tasks/install.mk @@ -1,20 +1,21 @@ ## Uninstall project's dependencies. uninstall: @$(call print_help,$@:) - @pip freeze | grep -v "^-e" | sed "s/@.*//" | xargs pip uninstall -y + @python -m pip freeze | grep -v "^-e" | sed "s/@.*//" | xargs pip uninstall -y ## Install project's overall dependencies install-deps: @$(call print_help,$@:) - @pip install --upgrade pip + @python -m pip install --upgrade pip ## Install project's development dependencies. install-edit: @$(call print_help,$@:) - @pip install --upgrade --editable ".[dev]" + @python -m pip install --upgrade --editable ".[dev]" ## Delete builds and compiled python files. clean: @$(call print_help,$@:) @ls -d * | grep "build\|dist" | xargs rm -rf + @find . -name "__pycache__" | xargs rm -rf @find . -name "*.pyc" | xargs rm -rf diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 946d07aa4b..646cf76d70 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -11,9 +11,9 @@ check-syntax-errors: . ## Run linters to check for syntax and style errors. check-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) - @isort --check $? - @black --check $? - @flake8 $? + @python -m isort --check $? + @python -m black --check $? + @python -m flake8 $? @$(call print_pass,$@:) ## Run linters to check for syntax and style errors in the doc. @@ -31,14 +31,14 @@ lint-doc-%: @## able to integrate documentation improvements progresively. @## @$(call print_help,$(subst $*,%,$@:)) - @flake8 --select=D101,D102,D103,DAR openfisca_core/$* - @pylint openfisca_core/$* + @python -m flake8 --select=D101,D102,D103,DAR openfisca_core/$* + @python -m pylint openfisca_core/$* @$(call print_pass,$@:) ## Run static type checkers for type errors. check-types: @$(call print_help,$@:) - @mypy \ + @python -m mypy \ openfisca_core/commons \ openfisca_core/entities \ openfisca_core/periods \ @@ -48,6 +48,6 @@ check-types: ## Run code formatters to correct style errors. format-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) - @isort $? - @black $? + @python -m isort $? + @python -m black $? @$(call print_pass,$@:) diff --git a/openfisca_tasks/publish.mk b/openfisca_tasks/publish.mk index 802c911fc8..37e599b63f 100644 --- a/openfisca_tasks/publish.mk +++ b/openfisca_tasks/publish.mk @@ -3,7 +3,7 @@ ## Install project's build dependencies. install-dist: @$(call print_help,$@:) - @pip install .[ci,dev] + @python -m pip install .[ci,dev] @$(call print_pass,$@:) ## Build & install openfisca-core for deployment and publishing. @@ -12,14 +12,14 @@ build: @## of openfisca-core, the same we put in the hands of users and reusers. @$(call print_help,$@:) @python -m build - @pip uninstall --yes openfisca-core - @find dist -name "*.whl" -exec pip install --no-deps {} \; + @python -m pip uninstall --yes openfisca-core + @find dist -name "*.whl" -exec python -m pip install --no-deps {} \; @$(call print_pass,$@:) ## Upload to PyPi. publish: @$(call print_help,$@:) - @twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN + @python -m twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN @git tag `python setup.py --version` @git push --tags # update the repository version @$(call print_pass,$@:) diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index 723c94ece0..8878fe9d33 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -1,8 +1,12 @@ ## The openfisca command module. openfisca = openfisca_core.scripts.openfisca_command -## The path to the installed packages. -python_packages = $(shell python -c "import sysconfig; print(sysconfig.get_paths()[\"purelib\"])") +## The path to the templates' tests. +ifeq ($(OS),Windows_NT) + tests = $(shell python -c "import os, $(1); print(repr(os.path.join($(1).__path__[0], 'tests')))") +else + tests = $(shell python -c "import $(1); print($(1).__path__[0])")/tests +endif ## Run all tasks required for testing. install: install-deps install-edit install-test @@ -10,8 +14,8 @@ install: install-deps install-edit install-test ## Enable regression testing with template repositories. install-test: @$(call print_help,$@:) - @pip install --upgrade --no-dependencies openfisca-country-template - @pip install --upgrade --no-dependencies openfisca-extension-template + @python -m pip install --upgrade --no-deps openfisca-country-template + @python -m pip install --upgrade --no-deps openfisca-extension-template ## Run openfisca-core & country/extension template tests. test-code: test-core test-country test-extension @@ -29,17 +33,17 @@ test-code: test-core test-country test-extension @$(call print_pass,$@:) ## Run openfisca-core tests. -test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 -d ":") +test-core: $(shell git ls-files "*test_*.py") @$(call print_help,$@:) - @pytest --capture=no --xdoctest --xdoctest-verbose=0 \ + @python -m pytest --capture=no --xdoctest --xdoctest-verbose=0 \ openfisca_core/commons \ openfisca_core/entities \ openfisca_core/holders \ openfisca_core/periods \ openfisca_core/projectors @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ - coverage run -m \ - ${openfisca} test $? \ + python -m coverage run -m ${openfisca} test \ + $? \ ${openfisca_args} @$(call print_pass,$@:) @@ -47,7 +51,8 @@ test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 test-country: @$(call print_help,$@:) @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ - openfisca test ${python_packages}/openfisca_country_template/tests \ + python -m ${openfisca} test \ + $(call tests,"openfisca_country_template") \ --country-package openfisca_country_template \ ${openfisca_args} @$(call print_pass,$@:) @@ -56,7 +61,8 @@ test-country: test-extension: @$(call print_help,$@:) @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ - openfisca test ${python_packages}/openfisca_extension_template/tests \ + python -m ${openfisca} test \ + $(call tests,"openfisca_extension_template") \ --country-package openfisca_country_template \ --extensions openfisca_extension_template \ ${openfisca_args} @@ -65,4 +71,5 @@ test-extension: ## Print the coverage report. test-cov: @$(call print_help,$@:) - @coverage report + @python -m coverage report + @$(call print_pass,$@:) From 98f5564720ac792155f4232e2991a9a42400525b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 29 Sep 2024 20:13:57 +0200 Subject: [PATCH 168/188] chore: upgrade upper numpy version --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0c021be25c..cbbbc4dbca 100644 --- a/setup.py +++ b/setup.py @@ -28,15 +28,14 @@ # DO NOT add space between '>=' and version number as it break conda build. general_requirements = [ "PyYAML >=6.0, <7.0", + "StrEnum >=0.4.8, <0.5.0", # 3.11.x backport "dpath >=2.1.4, <3.0", "numexpr >=2.8.4, <3.0", - "numpy >=1.24.2, <1.25", "pendulum >=3.0.0, <4.0.0", "psutil >=5.9.4, <6.0", "pytest >=8.3.3, <9.0", "sortedcontainers >=2.4.0, <3.0", "typing_extensions >=4.5.0, <5.0", - "StrEnum >=0.4.8, <0.5.0", # 3.11.x backport ] api_requirements = [ From bb1a63ecb566ead7c6a65721ad72100f5098085b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 30 Sep 2024 18:44:45 +0200 Subject: [PATCH 169/188] ci: remove extra comments --- .github/workflows/merge.yaml | 10 +++++----- .github/workflows/push.yaml | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index d52f83daa0..e6ebe3fe90 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -35,7 +35,7 @@ jobs: matrix: os: [ubuntu-22.04, windows-2019] numpy: [2.1.1, 1.24.2] - python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] include: - os: ubuntu-22.04 activate_command: source venv/bin/activate @@ -54,7 +54,7 @@ jobs: fail-fast: true matrix: numpy: [1.24.2] - python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] uses: ./.github/workflows/_lint.yaml with: os: ubuntu-22.04 @@ -78,7 +78,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.9.13 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python-version: 3.9.13 - name: Check version number has been properly updated run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" @@ -101,7 +101,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.9.13 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python-version: 3.9.13 - id: stop-early run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi # The `check-for-functional-changes` job should always succeed regardless of the `has-functional-changes` script's exit code. Consequently, we do not use that exit code to trigger deploy, but rather a dedicated output variable `status`, to avoid a job failure if the exit code is different from 0. Conversely, if the job fails the entire workflow would be marked as `failed` which is disturbing for contributors. @@ -122,7 +122,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.9.13 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python-version: 3.9.13 - name: Cache deps uses: actions/cache@v3 diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 1782500c8a..6e10600f3c 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -35,7 +35,7 @@ jobs: matrix: os: [ubuntu-22.04, windows-2019] numpy: [2.1.1, 1.24.2] - python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] include: - os: ubuntu-22.04 activate_command: source venv/bin/activate @@ -54,7 +54,7 @@ jobs: fail-fast: true matrix: numpy: [1.24.2] - python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] uses: ./.github/workflows/_lint.yaml with: os: ubuntu-22.04 @@ -78,7 +78,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: 3.9.13 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + python-version: 3.9.13 - name: Check version number has been properly updated run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" From c49b7227f30a43092cd72e7435324ac2cd42e3a8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 30 Sep 2024 19:07:48 +0200 Subject: [PATCH 170/188] style(lint): format with yamlfmt --- .github/dependabot.yml | 2 +- .github/workflows/_before.yaml | 140 ++++++++-------- .github/workflows/_lint.yaml | 57 +++---- .github/workflows/_test.yaml | 72 ++++---- .github/workflows/merge.yaml | 294 +++++++++++++++++---------------- .github/workflows/push.yaml | 49 +++--- 6 files changed, 323 insertions(+), 291 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fcb2acc162..71eaf02d67 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ version: 2 updates: - package-ecosystem: pip - directory: "/" + directory: / schedule: interval: monthly labels: diff --git a/.github/workflows/_before.yaml b/.github/workflows/_before.yaml index d347057bb3..561f36ad9f 100644 --- a/.github/workflows/_before.yaml +++ b/.github/workflows/_before.yaml @@ -24,79 +24,85 @@ jobs: runs-on: ${{ inputs.os }} name: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} env: - TERM: xterm-256color # To colorize output of make tasks. + # To colorize output of make tasks. + TERM: xterm-256color steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python }} - - - name: Use zstd for faster cache restore (windows) - if: ${{ startsWith(inputs.os, 'windows') }} - shell: cmd - run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - - - name: Cache dependencies - id: restore-deps - uses: actions/cache@v3 - with: - path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - restore-keys: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- - - - name: Install dependencies - run: | - python -m venv venv - ${{ inputs.activate_command }} - make install-deps install-dist + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + id: restore-deps + uses: actions/cache@v4 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }} + restore-keys: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python + }}- + + - name: Install dependencies + run: | + python -m venv venv + ${{ inputs.activate_command }} + make install-deps install-dist build: runs-on: ${{ inputs.os }} - needs: [ deps ] + needs: [deps] name: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} env: - TERM: xterm-256color # To colorize output of make tasks. + TERM: xterm-256color steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python }} - - - name: Use zstd for faster cache restore (windows) - if: ${{ startsWith(inputs.os, 'windows') }} - shell: cmd - run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - - - name: Cache build - uses: actions/cache@v3 - with: - path: venv/**/[Oo]pen[Ff]isca* - key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - restore-keys: | - build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- - build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- - - - name: Cache release - uses: actions/cache@v3 - with: - path: dist - key: release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Build package - run: | - ${{ inputs.activate_command }} - make install-test clean build + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }} + + - name: Cache build + uses: actions/cache@v4 + with: + path: venv/**/[Oo]pen[Ff]isca* + key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }}-${{ github.sha }} + restore-keys: | + build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- + build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- + + - name: Cache release + uses: actions/cache@v4 + with: + path: dist + key: release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }}-${{ github.sha }} + + - name: Build package + run: | + ${{ inputs.activate_command }} + make install-test clean build diff --git a/.github/workflows/_lint.yaml b/.github/workflows/_lint.yaml index 6f34c02b36..a65d0e5dbd 100644 --- a/.github/workflows/_lint.yaml +++ b/.github/workflows/_lint.yaml @@ -27,31 +27,32 @@ jobs: TERM: xterm-256color # To colorize output of make tasks. steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python }} - - - name: Use zstd for faster cache restore (windows) - if: ${{ startsWith(inputs.os, 'windows') }} - shell: cmd - run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - - - name: Lint doc - run: | - ${{ inputs.activate_command }} - make clean check-syntax-errors lint-doc - - - name: Lint styles - run: | - ${{ inputs.activate_command }} - make clean check-syntax-errors check-style + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }} + + - name: Lint doc + run: | + ${{ inputs.activate_command }} + make clean check-syntax-errors lint-doc + + - name: Lint styles + run: | + ${{ inputs.activate_command }} + make clean check-syntax-errors check-style diff --git a/.github/workflows/_test.yaml b/.github/workflows/_test.yaml index eda7224bb7..0a3a55ca87 100644 --- a/.github/workflows/_test.yaml +++ b/.github/workflows/_test.yaml @@ -28,45 +28,47 @@ jobs: TERM: xterm-256color # To colorize output of make tasks. steps: - - name: Checkout - uses: actions/checkout@v3 + - name: Checkout + uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python }} + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} - - name: Use zstd for faster cache restore (windows) - if: ${{ startsWith(inputs.os, 'windows') }} - shell: cmd - run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - - name: Cache dependencies - uses: actions/cache@v3 - with: - path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }} - - name: Cache build - uses: actions/cache@v3 - with: - path: venv/**/[Oo]pen[Ff]isca* - key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + - name: Cache build + uses: actions/cache@v4 + with: + path: venv/**/[Oo]pen[Ff]isca* + key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }}-${{ github.sha }} - - name: Run Openfisca Core tests - run: | - ${{ inputs.activate_command }} - make test-core - python -m coveralls --service=github + - name: Run Openfisca Core tests + run: | + ${{ inputs.activate_command }} + make test-core + python -m coveralls --service=github - - name: Run Country Template tests - if: ${{ startsWith(inputs.os, 'ubuntu') }} - run: | - ${{ inputs.activate_command }} - make test-country + - name: Run Country Template tests + if: ${{ startsWith(inputs.os, 'ubuntu') }} + run: | + ${{ inputs.activate_command }} + make test-country - - name: Run Extension Template tests - if: ${{ startsWith(inputs.os, 'ubuntu') }} - run: | - ${{ inputs.activate_command }} - make test-extension + - name: Run Extension Template tests + if: ${{ startsWith(inputs.os, 'ubuntu') }} + run: | + ${{ inputs.activate_command }} + make test-extension diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index e6ebe3fe90..fc3ac3fc31 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -15,7 +15,11 @@ jobs: matrix: os: [ubuntu-22.04, windows-2019] numpy: [2.1.1, 1.24.2] - python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + # Patch version must be specified to avoid any cache confusion, since + # the cache key depends on the full Python version. If left unspecified, + # different patch versions could be allocated between jobs, and any + # such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] include: - os: ubuntu-22.04 activate_command: source venv/bin/activate @@ -23,10 +27,10 @@ jobs: activate_command: .\venv\Scripts\activate uses: ./.github/workflows/_before.yaml with: - os: ${{ matrix.os }} - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: ${{ matrix.activate_command }} + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} test: needs: [setup] @@ -43,10 +47,10 @@ jobs: activate_command: .\venv\Scripts\activate uses: ./.github/workflows/_test.yaml with: - os: ${{ matrix.os }} - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: ${{ matrix.activate_command }} + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} lint: needs: [setup] @@ -57,10 +61,10 @@ jobs: python: [3.11.9, 3.9.13] uses: ./.github/workflows/_lint.yaml with: - os: ubuntu-22.04 - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: source venv/bin/activate + os: ubuntu-22.04 + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: source venv/bin/activate # The idea behind these dependencies is we want to give feedback to # contributors on the version number only after they have passed all tests, @@ -71,17 +75,18 @@ jobs: needs: [test, lint] steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Fetch all the tags + - uses: actions/checkout@v4 + with: + # Fetch all the tags + fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.9.13 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.9.13 - - name: Check version number has been properly updated - run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" + - name: Check version number has been properly updated + run: ${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh # GitHub Actions does not have a halt job option, to stop from deploying if # no functional changes were found. We build a separate job to substitute the @@ -89,22 +94,30 @@ jobs: # `check-for-functional-changes`job. check-for-functional-changes: runs-on: ubuntu-22.04 - needs: [check-version] # Last job to run + # Last job to run + needs: [check-version] outputs: status: ${{ steps.stop-early.outputs.status }} steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.9.13 - - - id: stop-early - run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi # The `check-for-functional-changes` job should always succeed regardless of the `has-functional-changes` script's exit code. Consequently, we do not use that exit code to trigger deploy, but rather a dedicated output variable `status`, to avoid a job failure if the exit code is different from 0. Conversely, if the job fails the entire workflow would be marked as `failed` which is disturbing for contributors. + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.9.13 + + - id: stop-early + # The `check-for-functional-changes` job should always succeed regardless + # of the `has-functional-changes` script's exit code. Consequently, we do + # not use that exit code to trigger deploy, but rather a dedicated output + # variable `status`, to avoid a job failure if the exit code is different + # from 0. Conversely, if the job fails the entire workflow would be + # marked as `failed` which is disturbing for contributors. + run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo + "::set-output name=status::success" ; fi publish-to-pypi: runs-on: ubuntu-22.04 @@ -115,47 +128,49 @@ jobs: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_OPENFISCA_BOT }} steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.9.13 - - - name: Cache deps - uses: actions/cache@v3 - with: - path: venv - key: deps-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }} - - - name: Cache build - uses: actions/cache@v3 - with: - path: venv/**/[oO]pen[fF]isca* - key: build-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Cache release - uses: actions/cache@v3 - with: - path: dist - key: release-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Upload package to PyPi - run: | - source venv/bin/activate - make publish - - - name: Update doc - run: | - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ - -d '{"ref":"master"}' + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.9.13 + + - name: Cache deps + uses: actions/cache@v4 + with: + path: venv + key: deps-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }} + + - name: Cache build + uses: actions/cache@v4 + with: + path: venv/**/[oO]pen[fF]isca* + key: build-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ + github.sha }} + + - name: Cache release + uses: actions/cache@v4 + with: + path: dist + key: release-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ + github.sha }} + + - name: Upload package to PyPi + run: | + source venv/bin/activate + make publish + + - name: Update doc + run: | + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ + -d '{"ref":"master"}' build-conda: runs-on: ubuntu-22.04 @@ -163,27 +178,27 @@ jobs: # Do not build on master, the artifact will be used if: github.ref != 'refs/heads/master' steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: "3.10.6" - # Add conda-forge for OpenFisca-Core - channels: conda-forge - activate-environment: true - - uses: actions/checkout@v3 - - name: Display version - run: echo "version=`python setup.py --version`" - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - - name: Build Conda package - run: conda build --croot /tmp/conda .conda - - name: Upload Conda build - uses: actions/upload-artifact@v3 - with: - name: conda-build-`python setup.py --version`-${{ github.sha }} - path: /tmp/conda + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: 3.10.6 + # Add conda-forge for OpenFisca-Core + channels: conda-forge + activate-environment: true + - uses: actions/checkout@v4 + - name: Display version + run: echo "version=`python setup.py --version`" + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + - name: Build Conda package + run: conda build --croot /tmp/conda .conda + - name: Upload Conda build + uses: actions/upload-artifact@v3 + with: + name: conda-build-`python setup.py --version`-${{ github.sha }} + path: /tmp/conda publish-to-conda: runs-on: ubuntu-22.04 @@ -192,51 +207,54 @@ jobs: fail-fast: false steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: 3.10.6 - channels: conda-forge - activate-environment: true - - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Update meta.yaml - run: | - python3 -m pip install requests argparse - # Sleep to allow PyPi to update its API - sleep 60 - python3 .github/get_pypi_info.py -p OpenFisca-Core - - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - conda config --set anaconda_upload yes - - - name: Conda build - run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user openfisca .conda + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: 3.10.6 + channels: conda-forge + activate-environment: true + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Update meta.yaml + run: | + python3 -m pip install requests argparse + # Sleep to allow PyPi to update its API + sleep 60 + python3 .github/get_pypi_info.py -p OpenFisca-Core + + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + conda config --set anaconda_upload yes + + - name: Conda build + run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user + openfisca .conda test-on-windows: runs-on: windows-2019 needs: [publish-to-conda] steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: "3.10.6" # See GHA Windows https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json - channels: conda-forge - activate-environment: true - - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Install with conda - run: conda install -c openfisca openfisca-core - - - name: Test openfisca - run: openfisca --help + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + # See GHA Windows + # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json + python-version: 3.10.6 + channels: conda-forge + activate-environment: true + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install with conda + run: conda install -c openfisca openfisca-core + + - name: Test openfisca + run: openfisca --help diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 6e10600f3c..b17a45c443 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -15,7 +15,11 @@ jobs: matrix: os: [ubuntu-22.04, windows-2019] numpy: [2.1.1, 1.24.2] - python: [3.11.9, 3.9.13] # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + # Patch version must be specified to avoid any cache confusion, since + # the cache key depends on the full Python version. If left unspecified, + # different patch versions could be allocated between jobs, and any + # such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] include: - os: ubuntu-22.04 activate_command: source venv/bin/activate @@ -23,10 +27,10 @@ jobs: activate_command: .\venv\Scripts\activate uses: ./.github/workflows/_before.yaml with: - os: ${{ matrix.os }} - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: ${{ matrix.activate_command }} + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} test: needs: [setup] @@ -43,10 +47,10 @@ jobs: activate_command: .\venv\Scripts\activate uses: ./.github/workflows/_test.yaml with: - os: ${{ matrix.os }} - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: ${{ matrix.activate_command }} + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} lint: needs: [setup] @@ -57,10 +61,10 @@ jobs: python: [3.11.9, 3.9.13] uses: ./.github/workflows/_lint.yaml with: - os: ubuntu-22.04 - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: source venv/bin/activate + os: ubuntu-22.04 + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: source venv/bin/activate # The idea behind these dependencies is we want to give feedback to # contributors on the version number only after they have passed all tests, @@ -71,14 +75,15 @@ jobs: needs: [test, lint] steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Fetch all the tags + - uses: actions/checkout@v4 + with: + # Fetch all the tags + fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.9.13 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.9.13 - - name: Check version number has been properly updated - run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" + - name: Check version number has been properly updated + run: ${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh From 7767e5854b74090c10ba79a5a1967a94133d0844 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 1 Oct 2024 17:11:14 +0200 Subject: [PATCH 171/188] ci(docs): use main ref: openfisca/openfisca-doc#309 --- .github/workflows/merge.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index fc3ac3fc31..b0175070fc 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -170,7 +170,7 @@ jobs: -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ - -d '{"ref":"master"}' + -d '{"ref":"main"}' build-conda: runs-on: ubuntu-22.04 From 1694ba2fd05726bdd125ec3ae95836c1614f2ffb Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 1 Oct 2024 19:55:05 +0200 Subject: [PATCH 172/188] ci: fix conda --- .github/workflows/merge.yaml | 28 ---------------------------- .github/workflows/push.yaml | 27 ++++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index b0175070fc..85c82b25a5 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -172,34 +172,6 @@ jobs: https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ -d '{"ref":"main"}' - build-conda: - runs-on: ubuntu-22.04 - needs: [setup] - # Do not build on master, the artifact will be used - if: github.ref != 'refs/heads/master' - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: 3.10.6 - # Add conda-forge for OpenFisca-Core - channels: conda-forge - activate-environment: true - - uses: actions/checkout@v4 - - name: Display version - run: echo "version=`python setup.py --version`" - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - - name: Build Conda package - run: conda build --croot /tmp/conda .conda - - name: Upload Conda build - uses: actions/upload-artifact@v3 - with: - name: conda-build-`python setup.py --version`-${{ github.sha }} - path: /tmp/conda - publish-to-conda: runs-on: ubuntu-22.04 needs: [publish-to-pypi] diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index b17a45c443..8b53dcad98 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -66,13 +66,38 @@ jobs: python: ${{ matrix.python }} activate_command: source venv/bin/activate + build-conda: + runs-on: ubuntu-22.04 + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: 3.10.6 + # Add conda-forge for OpenFisca-Core + channels: conda-forge + activate-environment: true + - uses: actions/checkout@v4 + - name: Display version + run: echo "version=`python setup.py --version`" + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + - name: Build Conda package + run: conda build --croot /tmp/conda .conda + - name: Upload Conda build + uses: actions/upload-artifact@v3 + with: + name: conda-build-`python setup.py --version`-${{ github.sha }} + path: /tmp/conda + # The idea behind these dependencies is we want to give feedback to # contributors on the version number only after they have passed all tests, # so they don't have to do it twice after changes happened to the main branch # during the time they took to fix the tests. check-version: runs-on: ubuntu-22.04 - needs: [test, lint] + needs: [test, lint, build-conda] steps: - uses: actions/checkout@v4 From 2f7da3f18390acaf1ba0f92c4202b6ff114b6d40 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 1 Oct 2024 17:16:39 +0200 Subject: [PATCH 173/188] chore: version bump --- CHANGELOG.md | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b914428e..82e0189fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +### 42.0.3 [#1234](https://github.com/openfisca/openfisca-core/pull/1234) + +#### Technical changes + +- Add matrix testing to CI + - Now it tests lower and upper bounds of python and numpy versions. + ### 42.0.2 [#1256](https://github.com/openfisca/openfisca-core/pull/1256) #### Documentation diff --git a/setup.py b/setup.py index cbbbc4dbca..0f5b393624 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup( name="OpenFisca-Core", - version="42.0.2", + version="42.0.3", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 17baccbcc9bd8e09c6a9208eee020f370a10c3b8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 2 Oct 2024 16:21:54 +0200 Subject: [PATCH 174/188] revert: "ci: add test matrix (#1234)" This reverts commit 0c3544a5ba34c106bf2b5b15acd44907aa0fbd80, reversing changes made to 1e894f4bdf2ec0cfc0f437db14cb9b599e47cf55. --- .github/dependabot.yml | 2 +- .github/publish-git-tag.sh | 4 + .github/workflows/_before.yaml | 108 ------------ .github/workflows/_lint.yaml | 58 ------ .github/workflows/_test.yaml | 74 -------- .github/workflows/merge.yaml | 232 ------------------------ .github/workflows/push.yaml | 114 ------------ .github/workflows/workflow.yml | 312 +++++++++++++++++++++++++++++++++ CHANGELOG.md | 7 - openfisca_tasks/install.mk | 7 +- openfisca_tasks/lint.mk | 16 +- openfisca_tasks/publish.mk | 14 +- openfisca_tasks/test_code.mk | 29 ++- setup.py | 5 +- 14 files changed, 345 insertions(+), 637 deletions(-) create mode 100755 .github/publish-git-tag.sh delete mode 100644 .github/workflows/_before.yaml delete mode 100644 .github/workflows/_lint.yaml delete mode 100644 .github/workflows/_test.yaml delete mode 100644 .github/workflows/merge.yaml delete mode 100644 .github/workflows/push.yaml create mode 100644 .github/workflows/workflow.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 71eaf02d67..fcb2acc162 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ version: 2 updates: - package-ecosystem: pip - directory: / + directory: "/" schedule: interval: monthly labels: diff --git a/.github/publish-git-tag.sh b/.github/publish-git-tag.sh new file mode 100755 index 0000000000..4450357cbc --- /dev/null +++ b/.github/publish-git-tag.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash + +git tag `python setup.py --version` +git push --tags # update the repository version diff --git a/.github/workflows/_before.yaml b/.github/workflows/_before.yaml deleted file mode 100644 index 561f36ad9f..0000000000 --- a/.github/workflows/_before.yaml +++ /dev/null @@ -1,108 +0,0 @@ -name: Setup package - -on: - workflow_call: - inputs: - os: - required: true - type: string - - numpy: - required: true - type: string - - python: - required: true - type: string - - activate_command: - required: true - type: string - -jobs: - deps: - runs-on: ${{ inputs.os }} - name: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} - env: - # To colorize output of make tasks. - TERM: xterm-256color - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ inputs.python }} - - - name: Use zstd for faster cache restore (windows) - if: ${{ startsWith(inputs.os, 'windows') }} - shell: cmd - run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - - - name: Cache dependencies - id: restore-deps - uses: actions/cache@v4 - with: - path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }} - restore-keys: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python - }}- - - - name: Install dependencies - run: | - python -m venv venv - ${{ inputs.activate_command }} - make install-deps install-dist - - build: - runs-on: ${{ inputs.os }} - needs: [deps] - name: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} - env: - TERM: xterm-256color - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ inputs.python }} - - - name: Use zstd for faster cache restore (windows) - if: ${{ startsWith(inputs.os, 'windows') }} - shell: cmd - run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }} - - - name: Cache build - uses: actions/cache@v4 - with: - path: venv/**/[Oo]pen[Ff]isca* - key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }}-${{ github.sha }} - restore-keys: | - build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- - build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- - - - name: Cache release - uses: actions/cache@v4 - with: - path: dist - key: release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }}-${{ github.sha }} - - - name: Build package - run: | - ${{ inputs.activate_command }} - make install-test clean build diff --git a/.github/workflows/_lint.yaml b/.github/workflows/_lint.yaml deleted file mode 100644 index a65d0e5dbd..0000000000 --- a/.github/workflows/_lint.yaml +++ /dev/null @@ -1,58 +0,0 @@ -name: Lint package - -on: - workflow_call: - inputs: - os: - required: true - type: string - - numpy: - required: true - type: string - - python: - required: true - type: string - - activate_command: - required: true - type: string - -jobs: - lint: - runs-on: ${{ inputs.os }} - name: lint-doc-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} - env: - TERM: xterm-256color # To colorize output of make tasks. - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ inputs.python }} - - - name: Use zstd for faster cache restore (windows) - if: ${{ startsWith(inputs.os, 'windows') }} - shell: cmd - run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }} - - - name: Lint doc - run: | - ${{ inputs.activate_command }} - make clean check-syntax-errors lint-doc - - - name: Lint styles - run: | - ${{ inputs.activate_command }} - make clean check-syntax-errors check-style diff --git a/.github/workflows/_test.yaml b/.github/workflows/_test.yaml deleted file mode 100644 index 0a3a55ca87..0000000000 --- a/.github/workflows/_test.yaml +++ /dev/null @@ -1,74 +0,0 @@ -name: Test package - -on: - workflow_call: - inputs: - os: - required: true - type: string - - numpy: - required: true - type: string - - python: - required: true - type: string - - activate_command: - required: true - type: string - -jobs: - test: - runs-on: ${{ inputs.os }} - name: test-core-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TERM: xterm-256color # To colorize output of make tasks. - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ inputs.python }} - - - name: Use zstd for faster cache restore (windows) - if: ${{ startsWith(inputs.os, 'windows') }} - shell: cmd - run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }} - - - name: Cache build - uses: actions/cache@v4 - with: - path: venv/**/[Oo]pen[Ff]isca* - key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }}-${{ github.sha }} - - - name: Run Openfisca Core tests - run: | - ${{ inputs.activate_command }} - make test-core - python -m coveralls --service=github - - - name: Run Country Template tests - if: ${{ startsWith(inputs.os, 'ubuntu') }} - run: | - ${{ inputs.activate_command }} - make test-country - - - name: Run Extension Template tests - if: ${{ startsWith(inputs.os, 'ubuntu') }} - run: | - ${{ inputs.activate_command }} - make test-extension diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml deleted file mode 100644 index 85c82b25a5..0000000000 --- a/.github/workflows/merge.yaml +++ /dev/null @@ -1,232 +0,0 @@ -name: OpenFisca-Core / Deploy package to PyPi & Conda - -on: - push: - branches: [master] - -concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - -jobs: - setup: - strategy: - fail-fast: true - matrix: - os: [ubuntu-22.04, windows-2019] - numpy: [2.1.1, 1.24.2] - # Patch version must be specified to avoid any cache confusion, since - # the cache key depends on the full Python version. If left unspecified, - # different patch versions could be allocated between jobs, and any - # such difference would lead to a cache not found error. - python: [3.11.9, 3.9.13] - include: - - os: ubuntu-22.04 - activate_command: source venv/bin/activate - - os: windows-2019 - activate_command: .\venv\Scripts\activate - uses: ./.github/workflows/_before.yaml - with: - os: ${{ matrix.os }} - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: ${{ matrix.activate_command }} - - test: - needs: [setup] - strategy: - fail-fast: true - matrix: - os: [ubuntu-22.04, windows-2019] - numpy: [2.1.1, 1.24.2] - python: [3.11.9, 3.9.13] - include: - - os: ubuntu-22.04 - activate_command: source venv/bin/activate - - os: windows-2019 - activate_command: .\venv\Scripts\activate - uses: ./.github/workflows/_test.yaml - with: - os: ${{ matrix.os }} - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: ${{ matrix.activate_command }} - - lint: - needs: [setup] - strategy: - fail-fast: true - matrix: - numpy: [1.24.2] - python: [3.11.9, 3.9.13] - uses: ./.github/workflows/_lint.yaml - with: - os: ubuntu-22.04 - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: source venv/bin/activate - - # The idea behind these dependencies is we want to give feedback to - # contributors on the version number only after they have passed all tests, - # so they don't have to do it twice after changes happened to the main branch - # during the time they took to fix the tests. - check-version: - runs-on: ubuntu-22.04 - needs: [test, lint] - - steps: - - uses: actions/checkout@v4 - with: - # Fetch all the tags - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.9.13 - - - name: Check version number has been properly updated - run: ${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh - - # GitHub Actions does not have a halt job option, to stop from deploying if - # no functional changes were found. We build a separate job to substitute the - # halt option. The `deploy` job is dependent on the output of the - # `check-for-functional-changes`job. - check-for-functional-changes: - runs-on: ubuntu-22.04 - # Last job to run - needs: [check-version] - outputs: - status: ${{ steps.stop-early.outputs.status }} - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.9.13 - - - id: stop-early - # The `check-for-functional-changes` job should always succeed regardless - # of the `has-functional-changes` script's exit code. Consequently, we do - # not use that exit code to trigger deploy, but rather a dedicated output - # variable `status`, to avoid a job failure if the exit code is different - # from 0. Conversely, if the job fails the entire workflow would be - # marked as `failed` which is disturbing for contributors. - run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo - "::set-output name=status::success" ; fi - - publish-to-pypi: - runs-on: ubuntu-22.04 - needs: [check-for-functional-changes] - if: needs.check-for-functional-changes.outputs.status == 'success' - env: - PYPI_USERNAME: __token__ - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_OPENFISCA_BOT }} - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.9.13 - - - name: Cache deps - uses: actions/cache@v4 - with: - path: venv - key: deps-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }} - - - name: Cache build - uses: actions/cache@v4 - with: - path: venv/**/[oO]pen[fF]isca* - key: build-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ - github.sha }} - - - name: Cache release - uses: actions/cache@v4 - with: - path: dist - key: release-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ - github.sha }} - - - name: Upload package to PyPi - run: | - source venv/bin/activate - make publish - - - name: Update doc - run: | - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ - -d '{"ref":"main"}' - - publish-to-conda: - runs-on: ubuntu-22.04 - needs: [publish-to-pypi] - strategy: - fail-fast: false - - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: 3.10.6 - channels: conda-forge - activate-environment: true - - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Update meta.yaml - run: | - python3 -m pip install requests argparse - # Sleep to allow PyPi to update its API - sleep 60 - python3 .github/get_pypi_info.py -p OpenFisca-Core - - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - conda config --set anaconda_upload yes - - - name: Conda build - run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user - openfisca .conda - - test-on-windows: - runs-on: windows-2019 - needs: [publish-to-conda] - - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - # See GHA Windows - # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json - python-version: 3.10.6 - channels: conda-forge - activate-environment: true - - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Install with conda - run: conda install -c openfisca openfisca-core - - - name: Test openfisca - run: openfisca --help diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml deleted file mode 100644 index 8b53dcad98..0000000000 --- a/.github/workflows/push.yaml +++ /dev/null @@ -1,114 +0,0 @@ -name: OpenFisca-Core / Pull request review - -on: - pull_request: - types: [assigned, opened, reopened, synchronize, ready_for_review] - -concurrency: - group: ${{ github.ref }} - cancel-in-progress: true - -jobs: - setup: - strategy: - fail-fast: true - matrix: - os: [ubuntu-22.04, windows-2019] - numpy: [2.1.1, 1.24.2] - # Patch version must be specified to avoid any cache confusion, since - # the cache key depends on the full Python version. If left unspecified, - # different patch versions could be allocated between jobs, and any - # such difference would lead to a cache not found error. - python: [3.11.9, 3.9.13] - include: - - os: ubuntu-22.04 - activate_command: source venv/bin/activate - - os: windows-2019 - activate_command: .\venv\Scripts\activate - uses: ./.github/workflows/_before.yaml - with: - os: ${{ matrix.os }} - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: ${{ matrix.activate_command }} - - test: - needs: [setup] - strategy: - fail-fast: true - matrix: - os: [ubuntu-22.04, windows-2019] - numpy: [2.1.1, 1.24.2] - python: [3.11.9, 3.9.13] - include: - - os: ubuntu-22.04 - activate_command: source venv/bin/activate - - os: windows-2019 - activate_command: .\venv\Scripts\activate - uses: ./.github/workflows/_test.yaml - with: - os: ${{ matrix.os }} - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: ${{ matrix.activate_command }} - - lint: - needs: [setup] - strategy: - fail-fast: true - matrix: - numpy: [1.24.2] - python: [3.11.9, 3.9.13] - uses: ./.github/workflows/_lint.yaml - with: - os: ubuntu-22.04 - numpy: ${{ matrix.numpy }} - python: ${{ matrix.python }} - activate_command: source venv/bin/activate - - build-conda: - runs-on: ubuntu-22.04 - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: 3.10.6 - # Add conda-forge for OpenFisca-Core - channels: conda-forge - activate-environment: true - - uses: actions/checkout@v4 - - name: Display version - run: echo "version=`python setup.py --version`" - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - - name: Build Conda package - run: conda build --croot /tmp/conda .conda - - name: Upload Conda build - uses: actions/upload-artifact@v3 - with: - name: conda-build-`python setup.py --version`-${{ github.sha }} - path: /tmp/conda - - # The idea behind these dependencies is we want to give feedback to - # contributors on the version number only after they have passed all tests, - # so they don't have to do it twice after changes happened to the main branch - # during the time they took to fix the tests. - check-version: - runs-on: ubuntu-22.04 - needs: [test, lint, build-conda] - - steps: - - uses: actions/checkout@v4 - with: - # Fetch all the tags - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.9.13 - - - name: Check version number has been properly updated - run: ${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000000..727a25e8c7 --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,312 @@ +name: OpenFisca-Core + +on: [ push, pull_request, workflow_dispatch ] + +jobs: + build: + runs-on: ubuntu-22.04 + env: + TERM: xterm-256color # To colorize output of make tasks. + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.10.6 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. + + - name: Cache build + id: restore-build + uses: actions/cache@v2 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + restore-keys: | # in case of a cache miss (systematically unless the same commit is built repeatedly), the keys below will be used to restore dependencies from previous builds, and the cache will be stored at the end of the job, making up-to-date dependencies available for all jobs of the workflow; see more at https://docs.github.com/en/actions/advanced-guides/caching-dependencies-to-speed-up-workflows#example-using-the-cache-action + build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }} + build-${{ env.pythonLocation }}- + + - name: Build package + run: make install-deps install-dist install-test clean build + + - name: Cache release + id: restore-release + uses: actions/cache@v2 + with: + path: dist + key: release-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + test-core: + runs-on: ubuntu-22.04 + needs: [ build ] + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TERM: xterm-256color # To colorize output of make tasks. + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.10.6 + + - name: Cache build + id: restore-build + uses: actions/cache@v2 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Run openfisca-core tests + run: make test-core + + - name: Submit coverage to Coveralls + run: coveralls --service=github + + test-country-template: + runs-on: ubuntu-22.04 + needs: [ build ] + env: + TERM: xterm-256color + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.10.6 + + - name: Cache build + id: restore-build + uses: actions/cache@v2 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Run Country Template tests + run: make test-country + + test-extension-template: + runs-on: ubuntu-22.04 + needs: [ build ] + env: + TERM: xterm-256color + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.10.6 + + - name: Cache build + id: restore-build + uses: actions/cache@v2 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Run Extension Template tests + run: make test-extension + + lint-files: + runs-on: ubuntu-22.04 + needs: [ build ] + env: + TERM: xterm-256color + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.10.6 + + - name: Cache build + id: restore-build + uses: actions/cache@v2 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Run linters + run: make lint + + check-version: + runs-on: ubuntu-22.04 + needs: [ test-core, test-country-template, test-extension-template, lint-files ] # Last job to run + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.10.6 + + - name: Check version number has been properly updated + run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" + + # GitHub Actions does not have a halt job option, to stop from deploying if no functional changes were found. + # We build a separate job to substitute the halt option. + # The `deploy` job is dependent on the output of the `check-for-functional-changes`job. + check-for-functional-changes: + runs-on: ubuntu-22.04 + if: github.ref == 'refs/heads/master' # Only triggered for the `master` branch + needs: [ check-version ] + outputs: + status: ${{ steps.stop-early.outputs.status }} + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.10.6 + + - id: stop-early + run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi # The `check-for-functional-changes` job should always succeed regardless of the `has-functional-changes` script's exit code. Consequently, we do not use that exit code to trigger deploy, but rather a dedicated output variable `status`, to avoid a job failure if the exit code is different from 0. Conversely, if the job fails the entire workflow would be marked as `failed` which is disturbing for contributors. + + deploy: + runs-on: ubuntu-22.04 + needs: [ check-for-functional-changes ] + if: needs.check-for-functional-changes.outputs.status == 'success' + env: + PYPI_USERNAME: __token__ + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_OPENFISCA_BOT }} + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.10.6 + + - name: Cache build + id: restore-build + uses: actions/cache@v2 + with: + path: ${{ env.pythonLocation }} + key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Cache release + id: restore-release + uses: actions/cache@v2 + with: + path: dist + key: release-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Upload a Python package to PyPi + run: twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN + + - name: Publish a git tag + run: "${GITHUB_WORKSPACE}/.github/publish-git-tag.sh" + + - name: Update doc + run: | + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ + -d '{"ref":"master"}' + + build-conda: + runs-on: ubuntu-22.04 + needs: [ build ] + # Do not build on master, the artifact will be used + if: github.ref != 'refs/heads/master' + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: "3.10.6" + # Add conda-forge for OpenFisca-Core + channels: conda-forge + activate-environment: true + - uses: actions/checkout@v3 + - name: Display version + run: echo "version=`python setup.py --version`" + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + - name: Build Conda package + run: conda build --croot /tmp/conda .conda + - name: Upload Conda build + uses: actions/upload-artifact@v3 + with: + name: conda-build-`python setup.py --version`-${{ github.sha }} + path: /tmp/conda + + publish-to-conda: + runs-on: "ubuntu-22.04" + needs: [ deploy ] + strategy: + fail-fast: false + + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: 3.10.6 + channels: conda-forge + activate-environment: true + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Update meta.yaml + run: | + python3 -m pip install requests argparse + # Sleep to allow PyPi to update its API + sleep 60 + python3 .github/get_pypi_info.py -p OpenFisca-Core + + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + conda config --set anaconda_upload yes + + - name: Conda build + run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user openfisca .conda + + test-on-windows: + runs-on: "windows-latest" + needs: [ publish-to-conda ] + + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: "3.10.6" # See GHA Windows https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json + channels: conda-forge + activate-environment: true + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Fetch all the tags + + - name: Install with conda + run: conda install -c openfisca openfisca-core + + - name: Test openfisca + run: openfisca --help diff --git a/CHANGELOG.md b/CHANGELOG.md index 82e0189fbc..49b914428e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,5 @@ # Changelog -### 42.0.3 [#1234](https://github.com/openfisca/openfisca-core/pull/1234) - -#### Technical changes - -- Add matrix testing to CI - - Now it tests lower and upper bounds of python and numpy versions. - ### 42.0.2 [#1256](https://github.com/openfisca/openfisca-core/pull/1256) #### Documentation diff --git a/openfisca_tasks/install.mk b/openfisca_tasks/install.mk index bb844b9d56..0a8c81115b 100644 --- a/openfisca_tasks/install.mk +++ b/openfisca_tasks/install.mk @@ -1,21 +1,20 @@ ## Uninstall project's dependencies. uninstall: @$(call print_help,$@:) - @python -m pip freeze | grep -v "^-e" | sed "s/@.*//" | xargs pip uninstall -y + @pip freeze | grep -v "^-e" | sed "s/@.*//" | xargs pip uninstall -y ## Install project's overall dependencies install-deps: @$(call print_help,$@:) - @python -m pip install --upgrade pip + @pip install --upgrade pip ## Install project's development dependencies. install-edit: @$(call print_help,$@:) - @python -m pip install --upgrade --editable ".[dev]" + @pip install --upgrade --editable ".[dev]" ## Delete builds and compiled python files. clean: @$(call print_help,$@:) @ls -d * | grep "build\|dist" | xargs rm -rf - @find . -name "__pycache__" | xargs rm -rf @find . -name "*.pyc" | xargs rm -rf diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 646cf76d70..946d07aa4b 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -11,9 +11,9 @@ check-syntax-errors: . ## Run linters to check for syntax and style errors. check-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) - @python -m isort --check $? - @python -m black --check $? - @python -m flake8 $? + @isort --check $? + @black --check $? + @flake8 $? @$(call print_pass,$@:) ## Run linters to check for syntax and style errors in the doc. @@ -31,14 +31,14 @@ lint-doc-%: @## able to integrate documentation improvements progresively. @## @$(call print_help,$(subst $*,%,$@:)) - @python -m flake8 --select=D101,D102,D103,DAR openfisca_core/$* - @python -m pylint openfisca_core/$* + @flake8 --select=D101,D102,D103,DAR openfisca_core/$* + @pylint openfisca_core/$* @$(call print_pass,$@:) ## Run static type checkers for type errors. check-types: @$(call print_help,$@:) - @python -m mypy \ + @mypy \ openfisca_core/commons \ openfisca_core/entities \ openfisca_core/periods \ @@ -48,6 +48,6 @@ check-types: ## Run code formatters to correct style errors. format-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) - @python -m isort $? - @python -m black $? + @isort $? + @black $? @$(call print_pass,$@:) diff --git a/openfisca_tasks/publish.mk b/openfisca_tasks/publish.mk index 37e599b63f..aeeb51141b 100644 --- a/openfisca_tasks/publish.mk +++ b/openfisca_tasks/publish.mk @@ -3,7 +3,7 @@ ## Install project's build dependencies. install-dist: @$(call print_help,$@:) - @python -m pip install .[ci,dev] + @pip install .[ci,dev] @$(call print_pass,$@:) ## Build & install openfisca-core for deployment and publishing. @@ -12,14 +12,6 @@ build: @## of openfisca-core, the same we put in the hands of users and reusers. @$(call print_help,$@:) @python -m build - @python -m pip uninstall --yes openfisca-core - @find dist -name "*.whl" -exec python -m pip install --no-deps {} \; - @$(call print_pass,$@:) - -## Upload to PyPi. -publish: - @$(call print_help,$@:) - @python -m twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN - @git tag `python setup.py --version` - @git push --tags # update the repository version + @pip uninstall --yes openfisca-core + @find dist -name "*.whl" -exec pip install --no-deps {} \; @$(call print_pass,$@:) diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index 8878fe9d33..723c94ece0 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -1,12 +1,8 @@ ## The openfisca command module. openfisca = openfisca_core.scripts.openfisca_command -## The path to the templates' tests. -ifeq ($(OS),Windows_NT) - tests = $(shell python -c "import os, $(1); print(repr(os.path.join($(1).__path__[0], 'tests')))") -else - tests = $(shell python -c "import $(1); print($(1).__path__[0])")/tests -endif +## The path to the installed packages. +python_packages = $(shell python -c "import sysconfig; print(sysconfig.get_paths()[\"purelib\"])") ## Run all tasks required for testing. install: install-deps install-edit install-test @@ -14,8 +10,8 @@ install: install-deps install-edit install-test ## Enable regression testing with template repositories. install-test: @$(call print_help,$@:) - @python -m pip install --upgrade --no-deps openfisca-country-template - @python -m pip install --upgrade --no-deps openfisca-extension-template + @pip install --upgrade --no-dependencies openfisca-country-template + @pip install --upgrade --no-dependencies openfisca-extension-template ## Run openfisca-core & country/extension template tests. test-code: test-core test-country test-extension @@ -33,17 +29,17 @@ test-code: test-core test-country test-extension @$(call print_pass,$@:) ## Run openfisca-core tests. -test-core: $(shell git ls-files "*test_*.py") +test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 -d ":") @$(call print_help,$@:) - @python -m pytest --capture=no --xdoctest --xdoctest-verbose=0 \ + @pytest --capture=no --xdoctest --xdoctest-verbose=0 \ openfisca_core/commons \ openfisca_core/entities \ openfisca_core/holders \ openfisca_core/periods \ openfisca_core/projectors @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ - python -m coverage run -m ${openfisca} test \ - $? \ + coverage run -m \ + ${openfisca} test $? \ ${openfisca_args} @$(call print_pass,$@:) @@ -51,8 +47,7 @@ test-core: $(shell git ls-files "*test_*.py") test-country: @$(call print_help,$@:) @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ - python -m ${openfisca} test \ - $(call tests,"openfisca_country_template") \ + openfisca test ${python_packages}/openfisca_country_template/tests \ --country-package openfisca_country_template \ ${openfisca_args} @$(call print_pass,$@:) @@ -61,8 +56,7 @@ test-country: test-extension: @$(call print_help,$@:) @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ - python -m ${openfisca} test \ - $(call tests,"openfisca_extension_template") \ + openfisca test ${python_packages}/openfisca_extension_template/tests \ --country-package openfisca_country_template \ --extensions openfisca_extension_template \ ${openfisca_args} @@ -71,5 +65,4 @@ test-extension: ## Print the coverage report. test-cov: @$(call print_help,$@:) - @python -m coverage report - @$(call print_pass,$@:) + @coverage report diff --git a/setup.py b/setup.py index 0f5b393624..0c021be25c 100644 --- a/setup.py +++ b/setup.py @@ -28,14 +28,15 @@ # DO NOT add space between '>=' and version number as it break conda build. general_requirements = [ "PyYAML >=6.0, <7.0", - "StrEnum >=0.4.8, <0.5.0", # 3.11.x backport "dpath >=2.1.4, <3.0", "numexpr >=2.8.4, <3.0", + "numpy >=1.24.2, <1.25", "pendulum >=3.0.0, <4.0.0", "psutil >=5.9.4, <6.0", "pytest >=8.3.3, <9.0", "sortedcontainers >=2.4.0, <3.0", "typing_extensions >=4.5.0, <5.0", + "StrEnum >=0.4.8, <0.5.0", # 3.11.x backport ] api_requirements = [ @@ -69,7 +70,7 @@ setup( name="OpenFisca-Core", - version="42.0.3", + version="42.0.2", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 8383fb221f22f3d2529346a8b102d8717fb5ec60 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 2 Oct 2024 15:53:57 +0200 Subject: [PATCH 175/188] test(commons): fix failing --- openfisca_core/commons/formulas.py | 17 ++++++++---- openfisca_core/commons/rates.py | 12 ++++++--- openfisca_core/commons/tests/test_dummy.py | 1 + openfisca_core/commons/tests/test_formulas.py | 26 ++++++++++++------- openfisca_core/commons/tests/test_rates.py | 12 ++++++--- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index bf1ba67c04..a184ad2dc4 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -56,8 +56,8 @@ def apply_thresholds( def concat( - this: t.Array[numpy.str_] | t.ArrayLike[str], - that: t.Array[numpy.str_] | t.ArrayLike[str], + this: t.Array[numpy.str_] | t.ArrayLike[object], + that: t.Array[numpy.str_] | t.ArrayLike[object], ) -> t.Array[numpy.str_]: """Concatenate the values of two arrays. @@ -75,17 +75,24 @@ def concat( array(['this1.0', 'that2.5']...) """ - if isinstance(this, numpy.ndarray) and not numpy.issubdtype(this.dtype, numpy.str_): + + if not isinstance(this, numpy.ndarray): + this = numpy.array(this) + + if 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 not isinstance(that, numpy.ndarray): + that = numpy.array(that) + + if not numpy.issubdtype(that.dtype, numpy.str_): that = that.astype("str") return numpy.char.add(this, that) def switch( - conditions: t.Array[numpy.float32], + conditions: t.Array[numpy.float32] | t.ArrayLike[float], value_by_condition: Mapping[float, float], ) -> t.Array[numpy.float32]: """Mimick a switch statement. diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index 11cb261980..cefc65406e 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -7,7 +7,7 @@ def average_rate( target: t.Array[numpy.float32], - varying: t.ArrayLike[float], + varying: t.Array[numpy.float32] | t.ArrayLike[float], trim: None | t.ArrayLike[float] = None, ) -> t.Array[numpy.float32]: """Compute the average rate of a target net income. @@ -38,7 +38,9 @@ def average_rate( """ - average_rate: t.Array[numpy.float32] + if not isinstance(varying, numpy.ndarray): + varying = numpy.array(varying, dtype=numpy.float32) + average_rate = 1 - target / varying if trim is not None: @@ -59,7 +61,7 @@ def average_rate( def marginal_rate( target: t.Array[numpy.float32], - varying: t.Array[numpy.float32], + varying: t.Array[numpy.float32] | t.ArrayLike[float], trim: None | t.ArrayLike[float] = None, ) -> t.Array[numpy.float32]: """Compute the marginal rate of a target net income. @@ -90,7 +92,9 @@ def marginal_rate( """ - marginal_rate: t.Array[numpy.float32] + if not isinstance(varying, numpy.ndarray): + varying = numpy.array(varying, dtype=numpy.float32) + marginal_rate = +1 - (target[:-1] - target[1:]) / (varying[:-1] - varying[1:]) if trim is not None: diff --git a/openfisca_core/commons/tests/test_dummy.py b/openfisca_core/commons/tests/test_dummy.py index 4dd13eabab..dfe04b3e44 100644 --- a/openfisca_core/commons/tests/test_dummy.py +++ b/openfisca_core/commons/tests/test_dummy.py @@ -5,5 +5,6 @@ def test_dummy_deprecation() -> None: """Dummy throws a deprecation warning when instantiated.""" + with pytest.warns(DeprecationWarning): assert Dummy() diff --git a/openfisca_core/commons/tests/test_formulas.py b/openfisca_core/commons/tests/test_formulas.py index 91866bd0c0..130df9505b 100644 --- a/openfisca_core/commons/tests/test_formulas.py +++ b/openfisca_core/commons/tests/test_formulas.py @@ -6,7 +6,8 @@ def test_apply_thresholds_when_several_inputs() -> None: - """Makes a choice for any given input.""" + """Make 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] @@ -17,7 +18,8 @@ def test_apply_thresholds_when_several_inputs() -> None: def test_apply_thresholds_when_too_many_thresholds() -> None: - """Raises an AssertionError when thresholds > choices.""" + """Raise an AssertionError when thresholds > choices.""" + input_ = numpy.array([6]) thresholds = [5, 7, 9, 11] choices = [10, 15, 20] @@ -27,7 +29,8 @@ def test_apply_thresholds_when_too_many_thresholds() -> None: def test_apply_thresholds_when_too_many_choices() -> None: - """Raises an AssertionError when thresholds < choices - 1.""" + """Raise an AssertionError when thresholds < choices - 1.""" + input_ = numpy.array([6]) thresholds = [5, 7] choices = [10, 15, 20, 25] @@ -37,7 +40,8 @@ def test_apply_thresholds_when_too_many_choices() -> None: def test_concat_when_this_is_array_not_str() -> None: - """Casts ``this`` to ``str`` when it is a NumPy array other than string.""" + """Cast ``this`` to ``str`` when it is a NumPy array other than string.""" + this = numpy.array([1, 2]) that = numpy.array(["la", "o"]) @@ -47,7 +51,8 @@ def test_concat_when_this_is_array_not_str() -> None: def test_concat_when_that_is_array_not_str() -> None: - """Casts ``that`` to ``str`` when it is a NumPy array other than string.""" + """Cast ``that`` to ``str`` when it is a NumPy array other than string.""" + this = numpy.array(["ho", "cha"]) that = numpy.array([1, 2]) @@ -57,16 +62,19 @@ def test_concat_when_that_is_array_not_str() -> None: def test_concat_when_args_not_str_array_like() -> None: - """Raises a TypeError when args are not a string array-like object.""" + """Cast ``this`` and ``that`` to a NumPy array or strings.""" + this = (1, 2) that = (3, 4) - with pytest.raises(TypeError): - commons.concat(this, that) + result = commons.concat(this, that) + + assert_array_equal(result, ["13", "24"]) def test_switch_when_values_are_empty() -> None: - """Raises an AssertionError when the values are empty.""" + """Raise an AssertionError when the values are empty.""" + conditions = [1, 1, 1, 2] value_by_condition = {} diff --git a/openfisca_core/commons/tests/test_rates.py b/openfisca_core/commons/tests/test_rates.py index 54e24b8d0f..c266582fc5 100644 --- a/openfisca_core/commons/tests/test_rates.py +++ b/openfisca_core/commons/tests/test_rates.py @@ -1,3 +1,5 @@ +import math + import numpy from numpy.testing import assert_array_equal @@ -5,20 +7,22 @@ def test_average_rate_when_varying_is_zero() -> None: - """Yields infinity when the varying gross income crosses zero.""" + """Yield 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]) + assert_array_equal(result, numpy.array([-math.inf, -math.inf, -math.inf])) def test_marginal_rate_when_varying_is_zero() -> None: - """Yields infinity when the varying gross income crosses zero.""" + """Yield 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]) + assert_array_equal(result, numpy.array([math.inf, math.inf])) From a3e49ed884449bf465b1c656189c1e8cda7aeae4 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 2 Oct 2024 16:40:45 +0200 Subject: [PATCH 176/188] ci: add matrix --- .github/dependabot.yml | 2 +- .github/publish-git-tag.sh | 4 - .github/workflows/_before.yaml | 109 ++++++++++++ .github/workflows/_lint.yaml | 58 ++++++ .github/workflows/_test.yaml | 74 ++++++++ .github/workflows/merge.yaml | 230 ++++++++++++++++++++++++ .github/workflows/push.yaml | 111 ++++++++++++ .github/workflows/workflow.yml | 312 --------------------------------- openfisca_tasks/install.mk | 7 +- openfisca_tasks/lint.mk | 16 +- openfisca_tasks/publish.mk | 14 +- openfisca_tasks/test_code.mk | 29 +-- setup.py | 4 +- 13 files changed, 626 insertions(+), 344 deletions(-) delete mode 100755 .github/publish-git-tag.sh create mode 100644 .github/workflows/_before.yaml create mode 100644 .github/workflows/_lint.yaml create mode 100644 .github/workflows/_test.yaml create mode 100644 .github/workflows/merge.yaml create mode 100644 .github/workflows/push.yaml delete mode 100644 .github/workflows/workflow.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fcb2acc162..71eaf02d67 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ version: 2 updates: - package-ecosystem: pip - directory: "/" + directory: / schedule: interval: monthly labels: diff --git a/.github/publish-git-tag.sh b/.github/publish-git-tag.sh deleted file mode 100755 index 4450357cbc..0000000000 --- a/.github/publish-git-tag.sh +++ /dev/null @@ -1,4 +0,0 @@ -#! /usr/bin/env bash - -git tag `python setup.py --version` -git push --tags # update the repository version diff --git a/.github/workflows/_before.yaml b/.github/workflows/_before.yaml new file mode 100644 index 0000000000..2cf6550884 --- /dev/null +++ b/.github/workflows/_before.yaml @@ -0,0 +1,109 @@ +name: Setup package + +on: + workflow_call: + inputs: + os: + required: true + type: string + + numpy: + required: true + type: string + + python: + required: true + type: string + + activate_command: + required: true + type: string + +jobs: + deps: + runs-on: ${{ inputs.os }} + name: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + env: + # To colorize output of make tasks. + TERM: xterm-256color + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + id: restore-deps + uses: actions/cache@v4 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }} + restore-keys: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python + }}- + + - name: Install dependencies + run: | + python -m venv venv + ${{ inputs.activate_command }} + make install-deps install-dist + pip install numpy==${{ inputs.numpy }} + + build: + runs-on: ${{ inputs.os }} + needs: [deps] + name: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + env: + TERM: xterm-256color + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }} + + - name: Cache build + uses: actions/cache@v4 + with: + path: venv/**/[Oo]pen[Ff]isca* + key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }}-${{ github.sha }} + restore-keys: | + build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- + build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- + + - name: Cache release + uses: actions/cache@v4 + with: + path: dist + key: release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }}-${{ github.sha }} + + - name: Build package + run: | + ${{ inputs.activate_command }} + make install-test clean build diff --git a/.github/workflows/_lint.yaml b/.github/workflows/_lint.yaml new file mode 100644 index 0000000000..a65d0e5dbd --- /dev/null +++ b/.github/workflows/_lint.yaml @@ -0,0 +1,58 @@ +name: Lint package + +on: + workflow_call: + inputs: + os: + required: true + type: string + + numpy: + required: true + type: string + + python: + required: true + type: string + + activate_command: + required: true + type: string + +jobs: + lint: + runs-on: ${{ inputs.os }} + name: lint-doc-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + env: + TERM: xterm-256color # To colorize output of make tasks. + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }} + + - name: Lint doc + run: | + ${{ inputs.activate_command }} + make clean check-syntax-errors lint-doc + + - name: Lint styles + run: | + ${{ inputs.activate_command }} + make clean check-syntax-errors check-style diff --git a/.github/workflows/_test.yaml b/.github/workflows/_test.yaml new file mode 100644 index 0000000000..0a3a55ca87 --- /dev/null +++ b/.github/workflows/_test.yaml @@ -0,0 +1,74 @@ +name: Test package + +on: + workflow_call: + inputs: + os: + required: true + type: string + + numpy: + required: true + type: string + + python: + required: true + type: string + + activate_command: + required: true + type: string + +jobs: + test: + runs-on: ${{ inputs.os }} + name: test-core-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TERM: xterm-256color # To colorize output of make tasks. + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} + + - name: Use zstd for faster cache restore (windows) + if: ${{ startsWith(inputs.os, 'windows') }} + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: venv + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }} + + - name: Cache build + uses: actions/cache@v4 + with: + path: venv/**/[Oo]pen[Ff]isca* + key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ + hashFiles('setup.py') }}-${{ github.sha }} + + - name: Run Openfisca Core tests + run: | + ${{ inputs.activate_command }} + make test-core + python -m coveralls --service=github + + - name: Run Country Template tests + if: ${{ startsWith(inputs.os, 'ubuntu') }} + run: | + ${{ inputs.activate_command }} + make test-country + + - name: Run Extension Template tests + if: ${{ startsWith(inputs.os, 'ubuntu') }} + run: | + ${{ inputs.activate_command }} + make test-extension diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml new file mode 100644 index 0000000000..0d490c6e47 --- /dev/null +++ b/.github/workflows/merge.yaml @@ -0,0 +1,230 @@ +name: OpenFisca-Core / Deploy package to PyPi & Conda + +on: + push: + branches: [master] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + setup: + strategy: + fail-fast: true + matrix: + os: [ubuntu-22.04, windows-2019] + numpy: [1.26.4, 1.24.2] + # Patch version must be specified to avoid any cache confusion, since + # the cache key depends on the full Python version. If left unspecified, + # different patch versions could be allocated between jobs, and any + # such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] + include: + - os: ubuntu-22.04 + activate_command: source venv/bin/activate + - os: windows-2019 + activate_command: .\venv\Scripts\activate + uses: ./.github/workflows/_before.yaml + with: + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} + + test: + needs: [setup] + strategy: + fail-fast: true + matrix: + os: [ubuntu-22.04, windows-2019] + numpy: [1.26.4, 1.24.2] + python: [3.11.9, 3.9.13] + include: + - os: ubuntu-22.04 + activate_command: source venv/bin/activate + - os: windows-2019 + activate_command: .\venv\Scripts\activate + uses: ./.github/workflows/_test.yaml + with: + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} + + lint: + needs: [setup] + strategy: + fail-fast: true + matrix: + numpy: [1.24.2] + python: [3.11.9, 3.9.13] + uses: ./.github/workflows/_lint.yaml + with: + os: ubuntu-22.04 + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: source venv/bin/activate + + # The idea behind these dependencies is we want to give feedback to + # contributors on the version number only after they have passed all tests, + # so they don't have to do it twice after changes happened to the main branch + # during the time they took to fix the tests. + check-version: + runs-on: ubuntu-22.04 + needs: [test, lint] + + steps: + - uses: actions/checkout@v4 + with: + # Fetch all the tags + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.9.13 + + - name: Check version number has been properly updated + run: ${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh + + # GitHub Actions does not have a halt job option, to stop from deploying if + # no functional changes were found. We build a separate job to substitute the + # halt option. The `deploy` job is dependent on the output of the + # `check-for-functional-changes`job. + check-for-functional-changes: + runs-on: ubuntu-22.04 + # Last job to run + needs: [check-version] + outputs: + status: ${{ steps.stop-early.outputs.status }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.9.13 + + - id: stop-early + # The `check-for-functional-changes` job should always succeed regardless + # of the `has-functional-changes` script's exit code. Consequently, we do + # not use that exit code to trigger deploy, but rather a dedicated output + # variable `status`, to avoid a job failure if the exit code is different + # from 0. Conversely, if the job fails the entire workflow would be + # marked as `failed` which is disturbing for contributors. + run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo + "::set-output name=status::success" ; fi + + publish-to-pypi: + runs-on: ubuntu-22.04 + needs: [check-for-functional-changes] + if: needs.check-for-functional-changes.outputs.status == 'success' + env: + PYPI_USERNAME: __token__ + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_OPENFISCA_BOT }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.9.13 + + - name: Cache deps + uses: actions/cache@v4 + with: + path: venv + key: deps-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }} + + - name: Cache build + uses: actions/cache@v4 + with: + path: venv/**/[oO]pen[fF]isca* + key: build-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ + github.sha }} + + - name: Cache release + uses: actions/cache@v4 + with: + path: dist + key: release-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ + github.sha }} + + - name: Upload package to PyPi + run: | + source venv/bin/activate + make publish + + - name: Update doc + run: | + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ + -d '{"ref":"main"}' + + publish-to-conda: + runs-on: ubuntu-22.04 + needs: [publish-to-pypi] + + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: 3.10.6 + channels: conda-forge + activate-environment: true + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Update meta.yaml + run: | + python3 -m pip install requests argparse + # Sleep to allow PyPi to update its API + sleep 60 + python3 .github/get_pypi_info.py -p OpenFisca-Core + + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + conda config --set anaconda_upload yes + + - name: Conda build + run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user + openfisca .conda + + test-on-windows: + runs-on: windows-2019 + needs: [publish-to-conda] + + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + # See GHA Windows + # https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json + python-version: 3.10.6 + channels: conda-forge + activate-environment: true + + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install with conda + run: conda install -c openfisca openfisca-core + + - name: Test openfisca + run: openfisca --help diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml new file mode 100644 index 0000000000..1fa07cb92d --- /dev/null +++ b/.github/workflows/push.yaml @@ -0,0 +1,111 @@ +name: OpenFisca-Core / Pull request review + +on: + pull_request: + types: [assigned, opened, reopened, synchronize, ready_for_review] + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + setup: + strategy: + fail-fast: true + matrix: + os: [ubuntu-22.04, windows-2019] + numpy: [1.26.4, 1.24.2] + # Patch version must be specified to avoid any cache confusion, since + # the cache key depends on the full Python version. If left unspecified, + # different patch versions could be allocated between jobs, and any + # such difference would lead to a cache not found error. + python: [3.11.9, 3.9.13] + include: + - os: ubuntu-22.04 + activate_command: source venv/bin/activate + - os: windows-2019 + activate_command: .\venv\Scripts\activate + uses: ./.github/workflows/_before.yaml + with: + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} + + test: + needs: [setup] + strategy: + fail-fast: true + matrix: + os: [ubuntu-22.04, windows-2019] + numpy: [1.26.4, 1.24.2] + python: [3.11.9, 3.9.13] + include: + - os: ubuntu-22.04 + activate_command: source venv/bin/activate + - os: windows-2019 + activate_command: .\venv\Scripts\activate + uses: ./.github/workflows/_test.yaml + with: + os: ${{ matrix.os }} + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: ${{ matrix.activate_command }} + + lint: + needs: [setup] + strategy: + fail-fast: true + matrix: + numpy: [1.24.2] + python: [3.11.9, 3.9.13] + uses: ./.github/workflows/_lint.yaml + with: + os: ubuntu-22.04 + numpy: ${{ matrix.numpy }} + python: ${{ matrix.python }} + activate_command: source venv/bin/activate + + build-conda: + runs-on: ubuntu-22.04 + steps: + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: 3.10.6 + # Add conda-forge for OpenFisca-Core + channels: conda-forge + activate-environment: true + - uses: actions/checkout@v4 + - name: Display version + run: echo "version=`python setup.py --version`" + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + - name: Build Conda package + run: conda build --croot /tmp/conda .conda + - name: Upload Conda build + uses: actions/upload-artifact@v3 + with: + name: conda-build-`python setup.py --version`-${{ github.sha }} + path: /tmp/conda + + # The idea behind these dependencies is that we want to give feedback to + # contributors on the version number only after they have passed all tests, + # so they don't have to do it twice after changes happened to the main branch + # during the time they took to fix the tests. + check-version: + runs-on: ubuntu-22.04 + needs: [test, lint, build-conda] + steps: + - uses: actions/checkout@v4 + with: + # Fetch all the tags + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.9.13 + - name: Check version number has been properly updated + run: ${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml deleted file mode 100644 index 727a25e8c7..0000000000 --- a/.github/workflows/workflow.yml +++ /dev/null @@ -1,312 +0,0 @@ -name: OpenFisca-Core - -on: [ push, pull_request, workflow_dispatch ] - -jobs: - build: - runs-on: ubuntu-22.04 - env: - TERM: xterm-256color # To colorize output of make tasks. - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 # Patch version must be specified to avoid any cache confusion, since the cache key depends on the full Python version. If left unspecified, different patch versions could be allocated between jobs, and any such difference would lead to a cache not found error. - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - restore-keys: | # in case of a cache miss (systematically unless the same commit is built repeatedly), the keys below will be used to restore dependencies from previous builds, and the cache will be stored at the end of the job, making up-to-date dependencies available for all jobs of the workflow; see more at https://docs.github.com/en/actions/advanced-guides/caching-dependencies-to-speed-up-workflows#example-using-the-cache-action - build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }} - build-${{ env.pythonLocation }}- - - - name: Build package - run: make install-deps install-dist install-test clean build - - - name: Cache release - id: restore-release - uses: actions/cache@v2 - with: - path: dist - key: release-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - test-core: - runs-on: ubuntu-22.04 - needs: [ build ] - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TERM: xterm-256color # To colorize output of make tasks. - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Run openfisca-core tests - run: make test-core - - - name: Submit coverage to Coveralls - run: coveralls --service=github - - test-country-template: - runs-on: ubuntu-22.04 - needs: [ build ] - env: - TERM: xterm-256color - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Run Country Template tests - run: make test-country - - test-extension-template: - runs-on: ubuntu-22.04 - needs: [ build ] - env: - TERM: xterm-256color - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Run Extension Template tests - run: make test-extension - - lint-files: - runs-on: ubuntu-22.04 - needs: [ build ] - env: - TERM: xterm-256color - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Run linters - run: make lint - - check-version: - runs-on: ubuntu-22.04 - needs: [ test-core, test-country-template, test-extension-template, lint-files ] # Last job to run - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Check version number has been properly updated - run: "${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh" - - # GitHub Actions does not have a halt job option, to stop from deploying if no functional changes were found. - # We build a separate job to substitute the halt option. - # The `deploy` job is dependent on the output of the `check-for-functional-changes`job. - check-for-functional-changes: - runs-on: ubuntu-22.04 - if: github.ref == 'refs/heads/master' # Only triggered for the `master` branch - needs: [ check-version ] - outputs: - status: ${{ steps.stop-early.outputs.status }} - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - id: stop-early - run: if "${GITHUB_WORKSPACE}/.github/has-functional-changes.sh" ; then echo "::set-output name=status::success" ; fi # The `check-for-functional-changes` job should always succeed regardless of the `has-functional-changes` script's exit code. Consequently, we do not use that exit code to trigger deploy, but rather a dedicated output variable `status`, to avoid a job failure if the exit code is different from 0. Conversely, if the job fails the entire workflow would be marked as `failed` which is disturbing for contributors. - - deploy: - runs-on: ubuntu-22.04 - needs: [ check-for-functional-changes ] - if: needs.check-for-functional-changes.outputs.status == 'success' - env: - PYPI_USERNAME: __token__ - PYPI_TOKEN: ${{ secrets.PYPI_TOKEN_OPENFISCA_BOT }} - - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: 3.10.6 - - - name: Cache build - id: restore-build - uses: actions/cache@v2 - with: - path: ${{ env.pythonLocation }} - key: build-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Cache release - id: restore-release - uses: actions/cache@v2 - with: - path: dist - key: release-${{ env.pythonLocation }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - - - name: Upload a Python package to PyPi - run: twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN - - - name: Publish a git tag - run: "${GITHUB_WORKSPACE}/.github/publish-git-tag.sh" - - - name: Update doc - run: | - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.OPENFISCADOC_BOT_ACCESS_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/openfisca/openfisca-doc/actions/workflows/deploy.yaml/dispatches \ - -d '{"ref":"master"}' - - build-conda: - runs-on: ubuntu-22.04 - needs: [ build ] - # Do not build on master, the artifact will be used - if: github.ref != 'refs/heads/master' - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: "3.10.6" - # Add conda-forge for OpenFisca-Core - channels: conda-forge - activate-environment: true - - uses: actions/checkout@v3 - - name: Display version - run: echo "version=`python setup.py --version`" - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - - name: Build Conda package - run: conda build --croot /tmp/conda .conda - - name: Upload Conda build - uses: actions/upload-artifact@v3 - with: - name: conda-build-`python setup.py --version`-${{ github.sha }} - path: /tmp/conda - - publish-to-conda: - runs-on: "ubuntu-22.04" - needs: [ deploy ] - strategy: - fail-fast: false - - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: 3.10.6 - channels: conda-forge - activate-environment: true - - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Update meta.yaml - run: | - python3 -m pip install requests argparse - # Sleep to allow PyPi to update its API - sleep 60 - python3 .github/get_pypi_info.py -p OpenFisca-Core - - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - conda config --set anaconda_upload yes - - - name: Conda build - run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user openfisca .conda - - test-on-windows: - runs-on: "windows-latest" - needs: [ publish-to-conda ] - - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: "3.10.6" # See GHA Windows https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json - channels: conda-forge - activate-environment: true - - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Fetch all the tags - - - name: Install with conda - run: conda install -c openfisca openfisca-core - - - name: Test openfisca - run: openfisca --help diff --git a/openfisca_tasks/install.mk b/openfisca_tasks/install.mk index 0a8c81115b..bb844b9d56 100644 --- a/openfisca_tasks/install.mk +++ b/openfisca_tasks/install.mk @@ -1,20 +1,21 @@ ## Uninstall project's dependencies. uninstall: @$(call print_help,$@:) - @pip freeze | grep -v "^-e" | sed "s/@.*//" | xargs pip uninstall -y + @python -m pip freeze | grep -v "^-e" | sed "s/@.*//" | xargs pip uninstall -y ## Install project's overall dependencies install-deps: @$(call print_help,$@:) - @pip install --upgrade pip + @python -m pip install --upgrade pip ## Install project's development dependencies. install-edit: @$(call print_help,$@:) - @pip install --upgrade --editable ".[dev]" + @python -m pip install --upgrade --editable ".[dev]" ## Delete builds and compiled python files. clean: @$(call print_help,$@:) @ls -d * | grep "build\|dist" | xargs rm -rf + @find . -name "__pycache__" | xargs rm -rf @find . -name "*.pyc" | xargs rm -rf diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 946d07aa4b..646cf76d70 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -11,9 +11,9 @@ check-syntax-errors: . ## Run linters to check for syntax and style errors. check-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) - @isort --check $? - @black --check $? - @flake8 $? + @python -m isort --check $? + @python -m black --check $? + @python -m flake8 $? @$(call print_pass,$@:) ## Run linters to check for syntax and style errors in the doc. @@ -31,14 +31,14 @@ lint-doc-%: @## able to integrate documentation improvements progresively. @## @$(call print_help,$(subst $*,%,$@:)) - @flake8 --select=D101,D102,D103,DAR openfisca_core/$* - @pylint openfisca_core/$* + @python -m flake8 --select=D101,D102,D103,DAR openfisca_core/$* + @python -m pylint openfisca_core/$* @$(call print_pass,$@:) ## Run static type checkers for type errors. check-types: @$(call print_help,$@:) - @mypy \ + @python -m mypy \ openfisca_core/commons \ openfisca_core/entities \ openfisca_core/periods \ @@ -48,6 +48,6 @@ check-types: ## Run code formatters to correct style errors. format-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) - @isort $? - @black $? + @python -m isort $? + @python -m black $? @$(call print_pass,$@:) diff --git a/openfisca_tasks/publish.mk b/openfisca_tasks/publish.mk index aeeb51141b..37e599b63f 100644 --- a/openfisca_tasks/publish.mk +++ b/openfisca_tasks/publish.mk @@ -3,7 +3,7 @@ ## Install project's build dependencies. install-dist: @$(call print_help,$@:) - @pip install .[ci,dev] + @python -m pip install .[ci,dev] @$(call print_pass,$@:) ## Build & install openfisca-core for deployment and publishing. @@ -12,6 +12,14 @@ build: @## of openfisca-core, the same we put in the hands of users and reusers. @$(call print_help,$@:) @python -m build - @pip uninstall --yes openfisca-core - @find dist -name "*.whl" -exec pip install --no-deps {} \; + @python -m pip uninstall --yes openfisca-core + @find dist -name "*.whl" -exec python -m pip install --no-deps {} \; + @$(call print_pass,$@:) + +## Upload to PyPi. +publish: + @$(call print_help,$@:) + @python -m twine upload dist/* --username $PYPI_USERNAME --password $PYPI_TOKEN + @git tag `python setup.py --version` + @git push --tags # update the repository version @$(call print_pass,$@:) diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index 723c94ece0..8878fe9d33 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -1,8 +1,12 @@ ## The openfisca command module. openfisca = openfisca_core.scripts.openfisca_command -## The path to the installed packages. -python_packages = $(shell python -c "import sysconfig; print(sysconfig.get_paths()[\"purelib\"])") +## The path to the templates' tests. +ifeq ($(OS),Windows_NT) + tests = $(shell python -c "import os, $(1); print(repr(os.path.join($(1).__path__[0], 'tests')))") +else + tests = $(shell python -c "import $(1); print($(1).__path__[0])")/tests +endif ## Run all tasks required for testing. install: install-deps install-edit install-test @@ -10,8 +14,8 @@ install: install-deps install-edit install-test ## Enable regression testing with template repositories. install-test: @$(call print_help,$@:) - @pip install --upgrade --no-dependencies openfisca-country-template - @pip install --upgrade --no-dependencies openfisca-extension-template + @python -m pip install --upgrade --no-deps openfisca-country-template + @python -m pip install --upgrade --no-deps openfisca-extension-template ## Run openfisca-core & country/extension template tests. test-code: test-core test-country test-extension @@ -29,17 +33,17 @@ test-code: test-core test-country test-extension @$(call print_pass,$@:) ## Run openfisca-core tests. -test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 -d ":") +test-core: $(shell git ls-files "*test_*.py") @$(call print_help,$@:) - @pytest --capture=no --xdoctest --xdoctest-verbose=0 \ + @python -m pytest --capture=no --xdoctest --xdoctest-verbose=0 \ openfisca_core/commons \ openfisca_core/entities \ openfisca_core/holders \ openfisca_core/periods \ openfisca_core/projectors @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ - coverage run -m \ - ${openfisca} test $? \ + python -m coverage run -m ${openfisca} test \ + $? \ ${openfisca_args} @$(call print_pass,$@:) @@ -47,7 +51,8 @@ test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 test-country: @$(call print_help,$@:) @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ - openfisca test ${python_packages}/openfisca_country_template/tests \ + python -m ${openfisca} test \ + $(call tests,"openfisca_country_template") \ --country-package openfisca_country_template \ ${openfisca_args} @$(call print_pass,$@:) @@ -56,7 +61,8 @@ test-country: test-extension: @$(call print_help,$@:) @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ - openfisca test ${python_packages}/openfisca_extension_template/tests \ + python -m ${openfisca} test \ + $(call tests,"openfisca_extension_template") \ --country-package openfisca_country_template \ --extensions openfisca_extension_template \ ${openfisca_args} @@ -65,4 +71,5 @@ test-extension: ## Print the coverage report. test-cov: @$(call print_help,$@:) - @coverage report + @python -m coverage report + @$(call print_pass,$@:) diff --git a/setup.py b/setup.py index 0c021be25c..7e5ed62c23 100644 --- a/setup.py +++ b/setup.py @@ -28,15 +28,15 @@ # DO NOT add space between '>=' and version number as it break conda build. general_requirements = [ "PyYAML >=6.0, <7.0", + "StrEnum >=0.4.8, <0.5.0", # 3.11.x backport "dpath >=2.1.4, <3.0", "numexpr >=2.8.4, <3.0", - "numpy >=1.24.2, <1.25", + "numpy >=1.24.2, <2.0", "pendulum >=3.0.0, <4.0.0", "psutil >=5.9.4, <6.0", "pytest >=8.3.3, <9.0", "sortedcontainers >=2.4.0, <3.0", "typing_extensions >=4.5.0, <5.0", - "StrEnum >=0.4.8, <0.5.0", # 3.11.x backport ] api_requirements = [ From 14ae2d5cf6d87494157a24625e23c21c21ed2e44 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 2 Oct 2024 17:15:12 +0200 Subject: [PATCH 177/188] ci: stage check version --- .github/workflows/_version.yaml | 39 +++++++++++++++++++++++++++++++++ .github/workflows/merge.yaml | 23 ++++--------------- .github/workflows/push.yaml | 20 ++++------------- openfisca_tasks/lint.mk | 1 + pyproject.toml | 2 +- setup.py | 1 + 6 files changed, 50 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/_version.yaml diff --git a/.github/workflows/_version.yaml b/.github/workflows/_version.yaml new file mode 100644 index 0000000000..dcb82a1509 --- /dev/null +++ b/.github/workflows/_version.yaml @@ -0,0 +1,39 @@ +name: Check version + +on: + workflow_call: + inputs: + os: + required: true + type: string + + python: + required: true + type: string + +jobs: + # The idea behind these dependencies is that we want to give feedback to + # contributors on the version number only after they have passed all tests, + # so they don't have to do it twice after changes happened to the main branch + # during the time they took to fix the tests. + check-version: + runs-on: ${{ inputs.os }} + name: check-version-${{ inputs.os }}-py${{ inputs.python }} + env: + # To colorize output of make tasks. + TERM: xterm-256color + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Fetch all the tags + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python }} + + - name: Check version number has been properly updated + run: ${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 0d490c6e47..cb66e6247e 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -66,27 +66,12 @@ jobs: python: ${{ matrix.python }} activate_command: source venv/bin/activate - # The idea behind these dependencies is we want to give feedback to - # contributors on the version number only after they have passed all tests, - # so they don't have to do it twice after changes happened to the main branch - # during the time they took to fix the tests. check-version: - runs-on: ubuntu-22.04 needs: [test, lint] - - steps: - - uses: actions/checkout@v4 - with: - # Fetch all the tags - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.9.13 - - - name: Check version number has been properly updated - run: ${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh + uses: ./.github/workflows/_version.yaml + with: + os: ubuntu-22.04 + python: 3.9.13 # GitHub Actions does not have a halt job option, to stop from deploying if # no functional changes were found. We build a separate job to substitute the diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 1fa07cb92d..1ae7eed6b9 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -91,21 +91,9 @@ jobs: name: conda-build-`python setup.py --version`-${{ github.sha }} path: /tmp/conda - # The idea behind these dependencies is that we want to give feedback to - # contributors on the version number only after they have passed all tests, - # so they don't have to do it twice after changes happened to the main branch - # during the time they took to fix the tests. check-version: - runs-on: ubuntu-22.04 needs: [test, lint, build-conda] - steps: - - uses: actions/checkout@v4 - with: - # Fetch all the tags - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.9.13 - - name: Check version number has been properly updated - run: ${GITHUB_WORKSPACE}/.github/is-version-number-acceptable.sh + uses: ./.github/workflows/_version.yaml + with: + os: ubuntu-22.04 + python: 3.9.13 diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 646cf76d70..fe2052dc55 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -50,4 +50,5 @@ format-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) @python -m isort $? @python -m black $? + @yamlfmt --write .github/**/*.yaml @$(call print_pass,$@:) diff --git a/pyproject.toml b/pyproject.toml index 1e2a43ee4e..ce4ba3779f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,2 @@ [tool.black] -target-version = ["py39", "py310", "py311"] +target-version = ["py39", "py310", "py311", "py312"] diff --git a/setup.py b/setup.py index 7e5ed62c23..944aa33043 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ "ruff >=0.6.7, <1.0", "ruff-lsp >=0.0.57, <1.0", "xdoctest >=1.2.0, <2.0", + "yamlfmt >= 1.1.1, <2.0", *api_requirements, ] From 0997ae3823955e3cfc683e05a9277517a175be25 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 2 Oct 2024 17:27:01 +0200 Subject: [PATCH 178/188] ci: stage conda build --- .github/workflows/_before-conda.yaml | 52 ++++++++++++++++++++++++++++ .github/workflows/push.yaml | 33 ++++-------------- 2 files changed, 59 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/_before-conda.yaml diff --git a/.github/workflows/_before-conda.yaml b/.github/workflows/_before-conda.yaml new file mode 100644 index 0000000000..f2cfe607e0 --- /dev/null +++ b/.github/workflows/_before-conda.yaml @@ -0,0 +1,52 @@ +name: Setup conda + +on: + workflow_call: + inputs: + os: + required: true + type: string + + python: + required: true + type: string + +jobs: + build-conda: + runs-on: ${{ inputs.os }} + name: build-conda-${{ inputs.os }}-py${{ inputs.python }} + env: + # To colorize output of make tasks. + TERM: xterm-256color + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Fetch all the tags + fetch-depth: 0 + + - uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: ${{ inputs.python }} + # Add conda-forge for OpenFisca-Core + channels: conda-forge + activate-environment: true + + - name: Display version + run: echo "version=`python setup.py --version`" + + - name: Conda Config + run: | + conda install conda-build anaconda-client + conda info + + - name: Build Conda package + run: conda build --croot /tmp/conda .conda + + - name: Upload Conda build + uses: actions/upload-artifact@v3 + with: + name: conda-build-`python setup.py --version`-${{ github.sha }} + path: /tmp/conda diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 1ae7eed6b9..cd11e0aeb4 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -32,6 +32,12 @@ jobs: python: ${{ matrix.python }} activate_command: ${{ matrix.activate_command }} + setup-conda: + uses: ./.github/workflows/_before-conda.yaml + with: + os: ubuntu-22.04 + python: 3.10.6 + test: needs: [setup] strategy: @@ -66,33 +72,8 @@ jobs: python: ${{ matrix.python }} activate_command: source venv/bin/activate - build-conda: - runs-on: ubuntu-22.04 - steps: - - uses: conda-incubator/setup-miniconda@v2 - with: - auto-update-conda: true - python-version: 3.10.6 - # Add conda-forge for OpenFisca-Core - channels: conda-forge - activate-environment: true - - uses: actions/checkout@v4 - - name: Display version - run: echo "version=`python setup.py --version`" - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - - name: Build Conda package - run: conda build --croot /tmp/conda .conda - - name: Upload Conda build - uses: actions/upload-artifact@v3 - with: - name: conda-build-`python setup.py --version`-${{ github.sha }} - path: /tmp/conda - check-version: - needs: [test, lint, build-conda] + needs: [test, lint, setup-conda] uses: ./.github/workflows/_version.yaml with: os: ubuntu-22.04 From 6fbaec0ea7c52441086692220368a5acda3f01aa Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 2 Oct 2024 17:34:41 +0200 Subject: [PATCH 179/188] ci: cache conda deps --- .github/workflows/_before-conda.yaml | 8 ++++++++ openfisca_tasks/lint.mk | 1 - setup.py | 1 - 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/_before-conda.yaml b/.github/workflows/_before-conda.yaml index f2cfe607e0..13726de29e 100644 --- a/.github/workflows/_before-conda.yaml +++ b/.github/workflows/_before-conda.yaml @@ -26,6 +26,14 @@ jobs: # Fetch all the tags fetch-depth: 0 + - name: Cache dependencies + id: restore-pkgs + uses: actions/cache@v4 + with: + path: ~/conda_pkgs_dir + key: pkgs-${{ inputs.os }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + restore-keys: pkgs-${{ inputs.os }}-py${{ inputs.python }}- + - uses: conda-incubator/setup-miniconda@v2 with: auto-update-conda: true diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index fe2052dc55..646cf76d70 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -50,5 +50,4 @@ format-style: $(shell git ls-files "*.py" "*.pyi") @$(call print_help,$@:) @python -m isort $? @python -m black $? - @yamlfmt --write .github/**/*.yaml @$(call print_pass,$@:) diff --git a/setup.py b/setup.py index 944aa33043..7e5ed62c23 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,6 @@ "ruff >=0.6.7, <1.0", "ruff-lsp >=0.0.57, <1.0", "xdoctest >=1.2.0, <2.0", - "yamlfmt >= 1.1.1, <2.0", *api_requirements, ] From 2c8d90eeb5b58b253f3cad74aa12b78f38fa05fa Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 2 Oct 2024 18:01:51 +0200 Subject: [PATCH 180/188] ci: fix conda cache --- .github/workflows/_before-conda.yaml | 99 +++++++++++++++++++++------- .github/workflows/_before.yaml | 16 ++--- .github/workflows/_lint.yaml | 3 +- .github/workflows/_test.yaml | 6 +- .github/workflows/push.yaml | 1 + conda_build_config.yaml | 9 +++ 6 files changed, 93 insertions(+), 41 deletions(-) create mode 100644 conda_build_config.yaml diff --git a/.github/workflows/_before-conda.yaml b/.github/workflows/_before-conda.yaml index 13726de29e..194295279a 100644 --- a/.github/workflows/_before-conda.yaml +++ b/.github/workflows/_before-conda.yaml @@ -7,14 +7,22 @@ on: required: true type: string + numpy: + required: true + type: string + python: required: true type: string +defaults: + run: + shell: bash -l {0} + jobs: - build-conda: + setup-conda: runs-on: ${{ inputs.os }} - name: build-conda-${{ inputs.os }}-py${{ inputs.python }} + name: setup-conda-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} env: # To colorize output of make tasks. TERM: xterm-256color @@ -22,39 +30,82 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + + - name: Cache conda env + uses: actions/cache@v4 with: - # Fetch all the tags - fetch-depth: 0 + path: | + /usr/share/miniconda/envs/openfisca + ~/.conda/envs/openfisca + .env.yaml + key: conda-env-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + restore-keys: conda-env-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- - - name: Cache dependencies - id: restore-pkgs + - name: Cache conda deps uses: actions/cache@v4 with: - path: ~/conda_pkgs_dir - key: pkgs-${{ inputs.os }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - restore-keys: pkgs-${{ inputs.os }}-py${{ inputs.python }}- + path: | + ~/conda_pkgs_dir + ~/conda-cache + key: conda-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + restore-keys: conda-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- + id: cache-env - - uses: conda-incubator/setup-miniconda@v2 + - name: Setup conda + uses: conda-incubator/setup-miniconda@v3 with: + activate-environment: openfisca auto-update-conda: true - python-version: ${{ inputs.python }} - # Add conda-forge for OpenFisca-Core channels: conda-forge - activate-environment: true + python-version: ${{ inputs.python }} + if: steps.cache-env.outputs.cache-hit != 'true' - - name: Display version - run: echo "version=`python setup.py --version`" + - name: Update conda + uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: openfisca + auto-update-conda: true + environment-file: .env.yaml + if: steps.cache-env.outputs.cache-hit == 'true' - - name: Conda Config + - name: Install dependencies run: | - conda install conda-build anaconda-client - conda info + conda install --name openfisca conda-build + conda install --name openfisca numpy=${{ inputs.numpy }} + conda install --name openfisca python=${{ inputs.python }} + if: steps.cache-env.outputs.cache-hit != 'true' + + - name: Update dependencies + run: conda env update --name openfisca --file .env.yaml + if: steps.cache-env.outputs.cache-hit == 'true' - - name: Build Conda package - run: conda build --croot /tmp/conda .conda + - name: Cache build + uses: actions/cache@v4 + with: + path: ~/conda-bld + key: conda-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + restore-keys: | + conda-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- + conda-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- - - name: Upload Conda build - uses: actions/upload-artifact@v3 + - name: Cache release + uses: actions/cache@v4 with: - name: conda-build-`python setup.py --version`-${{ github.sha }} - path: /tmp/conda + path: ~/conda-rel + key: conda-release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Build conda package + run: | + mkdir -p ~/conda-rel + conda build \ + .conda \ + --use-local \ + --build-only \ + --croot ~/conda-bld \ + --cache-dir ~/conda-cache \ + --output-folder ~/conda-rel \ + --numpy ${{ inputs.numpy }} \ + --python ${{ inputs.python }} + + - name: Export env + run: conda env export --name openfisca > .env.yaml diff --git a/.github/workflows/_before.yaml b/.github/workflows/_before.yaml index 2cf6550884..1aab3a6c5b 100644 --- a/.github/workflows/_before.yaml +++ b/.github/workflows/_before.yaml @@ -42,14 +42,11 @@ jobs: run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" - name: Cache dependencies - id: restore-deps uses: actions/cache@v4 with: path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }} - restore-keys: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python - }}- + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + restore-keys: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- - name: Install dependencies run: | @@ -83,15 +80,13 @@ jobs: uses: actions/cache@v4 with: path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }} + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - name: Cache build uses: actions/cache@v4 with: path: venv/**/[Oo]pen[Ff]isca* - key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }}-${{ github.sha }} + key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} restore-keys: | build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- @@ -100,8 +95,7 @@ jobs: uses: actions/cache@v4 with: path: dist - key: release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }}-${{ github.sha }} + key: release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - name: Build package run: | diff --git a/.github/workflows/_lint.yaml b/.github/workflows/_lint.yaml index a65d0e5dbd..ea7411e670 100644 --- a/.github/workflows/_lint.yaml +++ b/.github/workflows/_lint.yaml @@ -44,8 +44,7 @@ jobs: uses: actions/cache@v4 with: path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }} + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - name: Lint doc run: | diff --git a/.github/workflows/_test.yaml b/.github/workflows/_test.yaml index 0a3a55ca87..7133ff8c3c 100644 --- a/.github/workflows/_test.yaml +++ b/.github/workflows/_test.yaml @@ -45,15 +45,13 @@ jobs: uses: actions/cache@v4 with: path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }} + key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - name: Cache build uses: actions/cache@v4 with: path: venv/**/[Oo]pen[Ff]isca* - key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ - hashFiles('setup.py') }}-${{ github.sha }} + key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - name: Run Openfisca Core tests run: | diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index cd11e0aeb4..9699bd39e0 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -36,6 +36,7 @@ jobs: uses: ./.github/workflows/_before-conda.yaml with: os: ubuntu-22.04 + numpy: 1.26.4 python: 3.10.6 test: diff --git a/conda_build_config.yaml b/conda_build_config.yaml new file mode 100644 index 0000000000..55a6ec1bb9 --- /dev/null +++ b/conda_build_config.yaml @@ -0,0 +1,9 @@ +numpy: + - 1.24 + - 1.25 + - 1.26 + +python: + - 3.9 + - 3.10 + - 3.11 From 6370b4172980c63ac128ab42fc289362608d1078 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 3 Oct 2024 02:35:45 +0200 Subject: [PATCH 181/188] ci: test conda --- .conda/{ => openfisca-core}/README.md | 0 .conda/{ => openfisca-core}/meta.yaml | 26 ++----- .conda/openfisca-country-template/recipe.yaml | 43 ++++++++++++ .../openfisca-country-template/variants.yaml | 4 ++ .../openfisca-extension-template/recipe.yaml | 45 ++++++++++++ .../variants.yaml | 4 ++ .conda/pylint-per-file-ignores/recipe.yaml | 46 ++++++++++++ .conda/pylint-per-file-ignores/variants.yaml | 4 ++ .github/get_pypi_info.py | 2 +- .github/workflows/_before-conda.yaml | 70 ++++++++++--------- .../{_before.yaml => _before-pip.yaml} | 18 ++--- .../workflows/{_lint.yaml => _lint-pip.yaml} | 4 +- .github/workflows/_test-conda.yaml | 65 +++++++++++++++++ .../workflows/{_test.yaml => _test-pip.yaml} | 6 +- .github/workflows/merge.yaml | 51 ++++++++------ .github/workflows/push.yaml | 26 ++++--- conda_build_config.yaml | 9 --- 17 files changed, 316 insertions(+), 107 deletions(-) rename .conda/{ => openfisca-core}/README.md (100%) rename .conda/{ => openfisca-core}/meta.yaml (73%) create mode 100644 .conda/openfisca-country-template/recipe.yaml create mode 100644 .conda/openfisca-country-template/variants.yaml create mode 100644 .conda/openfisca-extension-template/recipe.yaml create mode 100644 .conda/openfisca-extension-template/variants.yaml create mode 100644 .conda/pylint-per-file-ignores/recipe.yaml create mode 100644 .conda/pylint-per-file-ignores/variants.yaml rename .github/workflows/{_before.yaml => _before-pip.yaml} (67%) rename .github/workflows/{_lint.yaml => _lint-pip.yaml} (86%) create mode 100644 .github/workflows/_test-conda.yaml rename .github/workflows/{_test.yaml => _test-pip.yaml} (82%) delete mode 100644 conda_build_config.yaml diff --git a/.conda/README.md b/.conda/openfisca-core/README.md similarity index 100% rename from .conda/README.md rename to .conda/openfisca-core/README.md diff --git a/.conda/meta.yaml b/.conda/openfisca-core/meta.yaml similarity index 73% rename from .conda/meta.yaml rename to .conda/openfisca-core/meta.yaml index abec48667e..26d73dff2b 100644 --- a/.conda/meta.yaml +++ b/.conda/openfisca-core/meta.yaml @@ -12,7 +12,7 @@ package: version: {{ version }} source: - path: .. + path: ../.. build: noarch: python @@ -27,20 +27,9 @@ requirements: - python - pip run: - {% for req in data.get('install_requires', []) %} + {% for req in data['install_requires'] %} - {{ req }} {% endfor %} - # - PyYAML >=6.0,<7.0 - # - dpath >=2.1.4,<3.0.0 - # - importlib-metadata >=6.1.0,<7.0 - # - numexpr >=2.8.4,<=3.0 - # - numpy >=1.24.2,<1.25.0 - # - pendulum >=2.1.2,<3.0.0 - # - psutil >=5.9.4,<6.0.0 - # - pytest >=7.2.2,<8.0.0 - # - python >=3.9,<4.0 - # - sortedcontainers >=2.4.0 - # - typing-extensions >=4.5.0,<5.0 test: imports: @@ -64,14 +53,9 @@ outputs: - python run: - python >=3.9,<4.0 - {% for req in data.get('api_requirements', []) %} + {% for req in data['extras_require']['web-api'] %} - {{ req }} {% endfor %} - # - # - flask >=2.2.3,<3.0 - # - flask-cors >=3.0.10,<4.0 - # - gunicorn >=20.1.0,<21.0.0 - # - werkzeug >=2.2.3,<3.0.0 - {{ pin_subpackage('openfisca-core', exact=True) }} - name: openfisca-core-dev @@ -82,7 +66,9 @@ outputs: - python run: - python >=3.9,<4.0 - {% for req in data.get('dev_requirements', []) %} + - openfisca-country-template + - openfisca-extension-template + {% for req in data['extras_require']['dev'] %} - {{ req }} {% endfor %} - {{ pin_subpackage('openfisca-core-api', exact=True) }} diff --git a/.conda/openfisca-country-template/recipe.yaml b/.conda/openfisca-country-template/recipe.yaml new file mode 100644 index 0000000000..c2feb7cc14 --- /dev/null +++ b/.conda/openfisca-country-template/recipe.yaml @@ -0,0 +1,43 @@ +schema_version: 1 + +context: + name: openfisca-country-template + version: 7.1.5 + +package: + name: ${{ name|lower }} + version: ${{ version }} + +source: + url: https://pypi.org/packages/source/${{ name[0] }}/${{ name }}/openfisca_country_template-${{ version }}.tar.gz + sha256: b2f2ac9945d9ccad467aed0925bd82f7f4d5ce4e96b212324cd071b8bee46914 + +build: + noarch: python + script: pip install . -v + +requirements: + host: + - python + - pip + run: + - python + +tests: +- python: + imports: + - openfisca_country_template +- requirements: + run: + - pip + script: + - pip check + +about: + summary: OpenFisca Rules as Code model for Country-Template. + license: AGPL-3.0 + license_file: LICENSE + +extra: + recipe-maintainers: + - bonjourmauko diff --git a/.conda/openfisca-country-template/variants.yaml b/.conda/openfisca-country-template/variants.yaml new file mode 100644 index 0000000000..15259d5160 --- /dev/null +++ b/.conda/openfisca-country-template/variants.yaml @@ -0,0 +1,4 @@ +python: +- 3.9 +- 3.10 +- 3.11 diff --git a/.conda/openfisca-extension-template/recipe.yaml b/.conda/openfisca-extension-template/recipe.yaml new file mode 100644 index 0000000000..e4a0ab6912 --- /dev/null +++ b/.conda/openfisca-extension-template/recipe.yaml @@ -0,0 +1,45 @@ +schema_version: 1 + +context: + name: openfisca-extension-template + version: 1.3.15 + +package: + name: ${{ name|lower }} + version: ${{ version }} + +source: + url: https://pypi.org/packages/source/${{ name[0] }}/${{ name }}/openfisca_extension_template-${{ version }}.tar.gz + sha256: e16ee9cbefdd5e9ddc1c2c0e12bcd74307c8cb1be55353b3b2788d64a90a5df9 + +build: + noarch: python + script: pip install . -v + +requirements: + host: + - python + - setuptools >=61.0 + - pip + run: + - python + +tests: +- python: + imports: + - openfisca_extension_template +- requirements: + run: + - pip + script: + - pip check + +about: + summary: An OpenFisca extension that adds some variables to an already-existing + tax and benefit system. + license: AGPL-3.0 + license_file: LICENSE + +extra: + recipe-maintainers: + - bonjourmauko diff --git a/.conda/openfisca-extension-template/variants.yaml b/.conda/openfisca-extension-template/variants.yaml new file mode 100644 index 0000000000..15259d5160 --- /dev/null +++ b/.conda/openfisca-extension-template/variants.yaml @@ -0,0 +1,4 @@ +python: +- 3.9 +- 3.10 +- 3.11 diff --git a/.conda/pylint-per-file-ignores/recipe.yaml b/.conda/pylint-per-file-ignores/recipe.yaml new file mode 100644 index 0000000000..05befd9567 --- /dev/null +++ b/.conda/pylint-per-file-ignores/recipe.yaml @@ -0,0 +1,46 @@ +schema_version: 1 + +context: + name: pylint-per-file-ignores + version: 1.3.2 + +package: + name: ${{ name|lower }} + version: ${{ version }} + +source: + url: https://pypi.org/packages/source/${{ name[0] }}/${{ name }}/pylint_per_file_ignores-${{ version }}.tar.gz + sha256: 3c641f69c316770749a8a353556504dae7469541cdaef38e195fe2228841451e + +build: + noarch: python + script: pip install . -v + +requirements: + host: + - python + - poetry-core >=1.0.0 + - pip + run: + - pylint >=3.3.1,<4.0 + - python + - tomli >=2.0.1,<3.0.0 + +tests: +- python: + imports: + - pylint_per_file_ignores +- requirements: + run: + - pip + script: + - pip check + +about: + summary: A pylint plugin to ignore error codes per file. + license: MIT + homepage: https://github.com/christopherpickering/pylint-per-file-ignores.git + +extra: + recipe-maintainers: + - bonjourmauko diff --git a/.conda/pylint-per-file-ignores/variants.yaml b/.conda/pylint-per-file-ignores/variants.yaml new file mode 100644 index 0000000000..15259d5160 --- /dev/null +++ b/.conda/pylint-per-file-ignores/variants.yaml @@ -0,0 +1,4 @@ +python: +- 3.9 +- 3.10 +- 3.11 diff --git a/.github/get_pypi_info.py b/.github/get_pypi_info.py index fd7a2c9238..70013fbe98 100644 --- a/.github/get_pypi_info.py +++ b/.github/get_pypi_info.py @@ -71,7 +71,7 @@ def replace_in_file(filepath: str, info: dict) -> None: "-f", "--filename", type=str, - default=".conda/meta.yaml", + default=".conda/openfisca-core/meta.yaml", help="Path to meta.yaml, with filename", ) args = parser.parse_args() diff --git a/.github/workflows/_before-conda.yaml b/.github/workflows/_before-conda.yaml index 194295279a..bbdc7f1bce 100644 --- a/.github/workflows/_before-conda.yaml +++ b/.github/workflows/_before-conda.yaml @@ -20,9 +20,9 @@ defaults: shell: bash -l {0} jobs: - setup-conda: + setup: runs-on: ${{ inputs.os }} - name: setup-conda-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + name: conda-setup-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} env: # To colorize output of make tasks. TERM: xterm-256color @@ -44,13 +44,17 @@ jobs: - name: Cache conda deps uses: actions/cache@v4 with: - path: | - ~/conda_pkgs_dir - ~/conda-cache + path: ~/conda_pkgs_dir key: conda-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} restore-keys: conda-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- id: cache-env + - name: Cache release + uses: actions/cache@v4 + with: + path: ~/conda-rel + key: conda-release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + - name: Setup conda uses: conda-incubator/setup-miniconda@v3 with: @@ -60,7 +64,14 @@ jobs: python-version: ${{ inputs.python }} if: steps.cache-env.outputs.cache-hit != 'true' - - name: Update conda + - name: Install dependencies + run: | + conda install conda-build conda-verify rattler-build + conda install numpy=${{ inputs.numpy }} + conda install python=${{ inputs.python }} + if: steps.cache-env.outputs.cache-hit != 'true' + + - name: Update conda & dependencies uses: conda-incubator/setup-miniconda@v3 with: activate-environment: openfisca @@ -68,41 +79,32 @@ jobs: environment-file: .env.yaml if: steps.cache-env.outputs.cache-hit == 'true' - - name: Install dependencies + - name: Build country template package run: | - conda install --name openfisca conda-build - conda install --name openfisca numpy=${{ inputs.numpy }} - conda install --name openfisca python=${{ inputs.python }} - if: steps.cache-env.outputs.cache-hit != 'true' - - - name: Update dependencies - run: conda env update --name openfisca --file .env.yaml - if: steps.cache-env.outputs.cache-hit == 'true' + rattler-build build \ + --recipe .conda/openfisca-country-template \ + --output-dir ~/conda-rel \ + --no-test - - name: Cache build - uses: actions/cache@v4 - with: - path: ~/conda-bld - key: conda-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - restore-keys: | - conda-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- - conda-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- + - name: Build extension template package + run: | + rattler-build build \ + --recipe .conda/openfisca-extension-template \ + --output-dir ~/conda-rel \ + --no-test - - name: Cache release - uses: actions/cache@v4 - with: - path: ~/conda-rel - key: conda-release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + - name: Build pylint plugin package + run: | + rattler-build build \ + --recipe .conda/pylint-per-file-ignores \ + --output-dir ~/conda-rel - - name: Build conda package + - name: Build core package run: | mkdir -p ~/conda-rel - conda build \ - .conda \ + conda build .conda/openfisca-core \ --use-local \ - --build-only \ - --croot ~/conda-bld \ - --cache-dir ~/conda-cache \ + --no-anaconda-upload \ --output-folder ~/conda-rel \ --numpy ${{ inputs.numpy }} \ --python ${{ inputs.python }} diff --git a/.github/workflows/_before.yaml b/.github/workflows/_before-pip.yaml similarity index 67% rename from .github/workflows/_before.yaml rename to .github/workflows/_before-pip.yaml index 1aab3a6c5b..02554419c8 100644 --- a/.github/workflows/_before.yaml +++ b/.github/workflows/_before-pip.yaml @@ -22,7 +22,7 @@ on: jobs: deps: runs-on: ${{ inputs.os }} - name: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + name: pip-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} env: # To colorize output of make tasks. TERM: xterm-256color @@ -45,8 +45,8 @@ jobs: uses: actions/cache@v4 with: path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - restore-keys: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- + key: pip-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + restore-keys: pip-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- - name: Install dependencies run: | @@ -58,7 +58,7 @@ jobs: build: runs-on: ${{ inputs.os }} needs: [deps] - name: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + name: pip-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} env: TERM: xterm-256color @@ -80,22 +80,22 @@ jobs: uses: actions/cache@v4 with: path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + key: pip-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - name: Cache build uses: actions/cache@v4 with: path: venv/**/[Oo]pen[Ff]isca* - key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + key: pip-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} restore-keys: | - build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- - build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- + pip-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}- + pip-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- - name: Cache release uses: actions/cache@v4 with: path: dist - key: release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + key: pip-release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - name: Build package run: | diff --git a/.github/workflows/_lint.yaml b/.github/workflows/_lint-pip.yaml similarity index 86% rename from .github/workflows/_lint.yaml rename to .github/workflows/_lint-pip.yaml index ea7411e670..e994f473e3 100644 --- a/.github/workflows/_lint.yaml +++ b/.github/workflows/_lint-pip.yaml @@ -22,7 +22,7 @@ on: jobs: lint: runs-on: ${{ inputs.os }} - name: lint-doc-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + name: pip-lint-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} env: TERM: xterm-256color # To colorize output of make tasks. @@ -44,7 +44,7 @@ jobs: uses: actions/cache@v4 with: path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + key: pip-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - name: Lint doc run: | diff --git a/.github/workflows/_test-conda.yaml b/.github/workflows/_test-conda.yaml new file mode 100644 index 0000000000..e5ac52529f --- /dev/null +++ b/.github/workflows/_test-conda.yaml @@ -0,0 +1,65 @@ +name: Test conda package + +on: + workflow_call: + inputs: + os: + required: true + type: string + + numpy: + required: true + type: string + + python: + required: true + type: string + +defaults: + run: + shell: bash -l {0} + +jobs: + test: + runs-on: ${{ inputs.os }} + name: conda-test-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + env: + TERM: xterm-256color # To colorize output of make tasks. + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache conda env + uses: actions/cache@v4 + with: + path: | + /usr/share/miniconda/envs/openfisca + ~/.conda/envs/openfisca + .env.yaml + key: conda-env-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + + - name: Cache conda deps + uses: actions/cache@v4 + with: + path: ~/conda_pkgs_dir + key: conda-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + + - name: Cache release + uses: actions/cache@v4 + with: + path: ~/conda-rel + key: conda-release-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + + - name: Update conda & dependencies + uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: openfisca + auto-update-conda: true + environment-file: .env.yaml + + - name: Install core + run: conda install openfisca-core-dev --channel file:///home/runner/conda-rel + + - name: Run core tests + run: make test-core diff --git a/.github/workflows/_test.yaml b/.github/workflows/_test-pip.yaml similarity index 82% rename from .github/workflows/_test.yaml rename to .github/workflows/_test-pip.yaml index 7133ff8c3c..704c7fbd17 100644 --- a/.github/workflows/_test.yaml +++ b/.github/workflows/_test-pip.yaml @@ -22,7 +22,7 @@ on: jobs: test: runs-on: ${{ inputs.os }} - name: test-core-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} + name: pip-test-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TERM: xterm-256color # To colorize output of make tasks. @@ -45,13 +45,13 @@ jobs: uses: actions/cache@v4 with: path: venv - key: deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} + key: pip-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} - name: Cache build uses: actions/cache@v4 with: path: venv/**/[Oo]pen[Ff]isca* - key: build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} + key: pip-build-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }}-${{ github.sha }} - name: Run Openfisca Core tests run: | diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index cb66e6247e..8a4b8f7fff 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -9,7 +9,7 @@ concurrency: cancel-in-progress: true jobs: - setup: + setup-pip: strategy: fail-fast: true matrix: @@ -25,15 +25,22 @@ jobs: activate_command: source venv/bin/activate - os: windows-2019 activate_command: .\venv\Scripts\activate - uses: ./.github/workflows/_before.yaml + uses: ./.github/workflows/_before-pip.yaml with: os: ${{ matrix.os }} numpy: ${{ matrix.numpy }} python: ${{ matrix.python }} activate_command: ${{ matrix.activate_command }} - test: - needs: [setup] + setup-conda: + uses: ./.github/workflows/_before-conda.yaml + with: + os: ubuntu-22.04 + numpy: 1.26.4 + python: 3.10.6 + + test-pip: + needs: [setup-pip] strategy: fail-fast: true matrix: @@ -45,21 +52,29 @@ jobs: activate_command: source venv/bin/activate - os: windows-2019 activate_command: .\venv\Scripts\activate - uses: ./.github/workflows/_test.yaml + uses: ./.github/workflows/_test-pip.yaml with: os: ${{ matrix.os }} numpy: ${{ matrix.numpy }} python: ${{ matrix.python }} activate_command: ${{ matrix.activate_command }} - lint: - needs: [setup] + test-conda: + uses: ./.github/workflows/_test-conda.yaml + needs: [setup-conda] + with: + os: ubuntu-22.04 + numpy: 1.26.4 + python: 3.10.6 + + lint-pip: + needs: [setup-pip] strategy: fail-fast: true matrix: numpy: [1.24.2] python: [3.11.9, 3.9.13] - uses: ./.github/workflows/_lint.yaml + uses: ./.github/workflows/_lint-pip.yaml with: os: ubuntu-22.04 numpy: ${{ matrix.numpy }} @@ -67,7 +82,7 @@ jobs: activate_command: source venv/bin/activate check-version: - needs: [test, lint] + needs: [test-pip, test-conda, lint-pip] uses: ./.github/workflows/_version.yaml with: os: ubuntu-22.04 @@ -126,21 +141,19 @@ jobs: uses: actions/cache@v4 with: path: venv - key: deps-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }} + key: pip-deps-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }} - name: Cache build uses: actions/cache@v4 with: path: venv/**/[oO]pen[fF]isca* - key: build-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ - github.sha }} + key: pip-build-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ github.sha }} - name: Cache release uses: actions/cache@v4 with: path: dist - key: release-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ - github.sha }} + key: pip-release-ubuntu-22.04-np1.24.2-py3.9.13-${{ hashFiles('setup.py') }}-${{ github.sha }} - name: Upload package to PyPi run: | @@ -169,9 +182,8 @@ jobs: channels: conda-forge activate-environment: true - - uses: actions/checkout@v2 - with: - fetch-depth: 0 + - name: Checkout + uses: actions/checkout@v2 - name: Update meta.yaml run: | @@ -204,9 +216,8 @@ jobs: channels: conda-forge activate-environment: true - - uses: actions/checkout@v2 - with: - fetch-depth: 0 + - name: Checkout + uses: actions/checkout@v2 - name: Install with conda run: conda install -c openfisca openfisca-core diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 9699bd39e0..2a8fbdef76 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -9,7 +9,7 @@ concurrency: cancel-in-progress: true jobs: - setup: + setup-pip: strategy: fail-fast: true matrix: @@ -25,7 +25,7 @@ jobs: activate_command: source venv/bin/activate - os: windows-2019 activate_command: .\venv\Scripts\activate - uses: ./.github/workflows/_before.yaml + uses: ./.github/workflows/_before-pip.yaml with: os: ${{ matrix.os }} numpy: ${{ matrix.numpy }} @@ -39,8 +39,8 @@ jobs: numpy: 1.26.4 python: 3.10.6 - test: - needs: [setup] + test-pip: + needs: [setup-pip] strategy: fail-fast: true matrix: @@ -52,21 +52,29 @@ jobs: activate_command: source venv/bin/activate - os: windows-2019 activate_command: .\venv\Scripts\activate - uses: ./.github/workflows/_test.yaml + uses: ./.github/workflows/_test-pip.yaml with: os: ${{ matrix.os }} numpy: ${{ matrix.numpy }} python: ${{ matrix.python }} activate_command: ${{ matrix.activate_command }} - lint: - needs: [setup] + test-conda: + uses: ./.github/workflows/_test-conda.yaml + needs: [setup-conda] + with: + os: ubuntu-22.04 + numpy: 1.26.4 + python: 3.10.6 + + lint-pip: + needs: [setup-pip] strategy: fail-fast: true matrix: numpy: [1.24.2] python: [3.11.9, 3.9.13] - uses: ./.github/workflows/_lint.yaml + uses: ./.github/workflows/_lint-pip.yaml with: os: ubuntu-22.04 numpy: ${{ matrix.numpy }} @@ -74,7 +82,7 @@ jobs: activate_command: source venv/bin/activate check-version: - needs: [test, lint, setup-conda] + needs: [test-pip, test-conda, lint-pip] uses: ./.github/workflows/_version.yaml with: os: ubuntu-22.04 diff --git a/conda_build_config.yaml b/conda_build_config.yaml deleted file mode 100644 index 55a6ec1bb9..0000000000 --- a/conda_build_config.yaml +++ /dev/null @@ -1,9 +0,0 @@ -numpy: - - 1.24 - - 1.25 - - 1.26 - -python: - - 3.9 - - 3.10 - - 3.11 From ce127232c44d431f1cece8b42cf758d30d118c2a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 3 Oct 2024 17:32:30 +0200 Subject: [PATCH 182/188] ci: add missing tests to conda --- .github/workflows/_test-conda.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/_test-conda.yaml b/.github/workflows/_test-conda.yaml index e5ac52529f..e1a6986740 100644 --- a/.github/workflows/_test-conda.yaml +++ b/.github/workflows/_test-conda.yaml @@ -63,3 +63,9 @@ jobs: - name: Run core tests run: make test-core + + - name: Run country tests + run: make test-country + + - name: Run extension tests + run: make test-extension From d1ea49d72e8ea85b2b0a76314b62515865f27821 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 3 Oct 2024 17:59:44 +0200 Subject: [PATCH 183/188] ci: update conda publish --- .github/workflows/_before-conda.yaml | 2 +- .github/workflows/merge.yaml | 69 ++++++++++++++++++---------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/.github/workflows/_before-conda.yaml b/.github/workflows/_before-conda.yaml index bbdc7f1bce..f202a1eb35 100644 --- a/.github/workflows/_before-conda.yaml +++ b/.github/workflows/_before-conda.yaml @@ -66,7 +66,7 @@ jobs: - name: Install dependencies run: | - conda install conda-build conda-verify rattler-build + conda install conda-build conda-verify rattler-build anaconda-client conda install numpy=${{ inputs.numpy }} conda install python=${{ inputs.python }} if: steps.cache-env.outputs.cache-hit != 'true' diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 8a4b8f7fff..f6753b400f 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -175,39 +175,62 @@ jobs: needs: [publish-to-pypi] steps: - - uses: conda-incubator/setup-miniconda@v2 + - name: Checkout + uses: actions/checkout@v4 + + - name: Cache conda env + uses: actions/cache@v4 with: - auto-update-conda: true - python-version: 3.10.6 - channels: conda-forge - activate-environment: true + path: | + /usr/share/miniconda/envs/openfisca + ~/.conda/envs/openfisca + .env.yaml + key: conda-env-ubuntu-22.04-np1.26.4-py3.10.6-${{ hashFiles('setup.py') }} - - name: Checkout - uses: actions/checkout@v2 + - name: Cache conda deps + uses: actions/cache@v4 + with: + path: ~/conda_pkgs_dir + key: conda-deps-ubuntu-22.04-np1.26.4-py3.10.6-${{ hashFiles('setup.py') }} - - name: Update meta.yaml - run: | - python3 -m pip install requests argparse - # Sleep to allow PyPi to update its API - sleep 60 - python3 .github/get_pypi_info.py -p OpenFisca-Core + - name: Cache release + uses: actions/cache@v4 + with: + path: ~/conda-rel + key: conda-release-ubuntu-22.04-np1.26.4-py3.10.6-${{ hashFiles('setup.py') }}-${{ github.sha }} - - name: Conda Config - run: | - conda install conda-build anaconda-client - conda info - conda config --set anaconda_upload yes + - name: Update conda & dependencies + uses: conda-incubator/setup-miniconda@v3 + with: + activate-environment: openfisca + auto-update-conda: true + environment-file: .env.yaml - - name: Conda build - run: conda build -c conda-forge --token ${{ secrets.ANACONDA_TOKEN }} --user - openfisca .conda + - name: Publish to conda + run: | + anaconda upload ~/conda-rel/noarch/openfisca-extension-template-* \ + --token ${{ secrets.ANACONDA_TOKEN }} + --user openfisca + --force + anaconda upload ~/conda-rel/noarch/openfisca-country-template-* \ + --token ${{ secrets.ANACONDA_TOKEN }} + --user openfisca + --force + anaconda upload ~/conda-rel/noarch/pylint-per-file-ignores-* \ + --token ${{ secrets.ANACONDA_TOKEN }} + --user openfisca + --force + anaconda upload ~/conda-rel/noarch/openfisca-core-* \ + --token ${{ secrets.ANACONDA_TOKEN }} + --user openfisca + --force test-on-windows: runs-on: windows-2019 needs: [publish-to-conda] steps: - - uses: conda-incubator/setup-miniconda@v2 + - uses: conda-incubator/setup-miniconda@v3 with: auto-update-conda: true # See GHA Windows @@ -217,7 +240,7 @@ jobs: activate-environment: true - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install with conda run: conda install -c openfisca openfisca-core From e9cb42380b986b5495d00bdde59075c6751db095 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 3 Oct 2024 21:53:06 +0200 Subject: [PATCH 184/188] ci: fix build order --- .conda/openfisca-core/conda_build_config.yaml | 9 ++++ .conda/openfisca-core/meta.yaml | 19 ++++---- .conda/openfisca-country-template/recipe.yaml | 11 +++-- .../openfisca-country-template/variants.yaml | 7 +-- .../openfisca-extension-template/recipe.yaml | 10 ++--- .../variants.yaml | 7 +-- .conda/pylint-per-file-ignores/recipe.yaml | 5 --- .conda/pylint-per-file-ignores/variants.yaml | 4 +- .github/workflows/_before-conda.yaml | 44 +++++++++---------- .github/workflows/_test-conda.yaml | 11 +++-- .github/workflows/merge.yaml | 21 ++++----- .github/workflows/push.yaml | 2 +- 12 files changed, 73 insertions(+), 77 deletions(-) create mode 100644 .conda/openfisca-core/conda_build_config.yaml diff --git a/.conda/openfisca-core/conda_build_config.yaml b/.conda/openfisca-core/conda_build_config.yaml new file mode 100644 index 0000000000..02754f3894 --- /dev/null +++ b/.conda/openfisca-core/conda_build_config.yaml @@ -0,0 +1,9 @@ +numpy: +- 1.24 +- 1.25 +- 1.26 + +python: +- 3.9 +- 3.10 +- 3.11 diff --git a/.conda/openfisca-core/meta.yaml b/.conda/openfisca-core/meta.yaml index 26d73dff2b..88df5d411d 100644 --- a/.conda/openfisca-core/meta.yaml +++ b/.conda/openfisca-core/meta.yaml @@ -24,23 +24,22 @@ build: requirements: host: - - python - pip + - python + - setuptools >=61.0 run: + - numpy + - python {% for req in data['install_requires'] %} + {% if not req.startswith('numpy') %} - {{ req }} + {% endif %} {% endfor %} test: imports: - openfisca_core - openfisca_core.commons - requires: - - pip - commands: - - pip check - - openfisca --help - - openfisca-run-test --help outputs: - name: openfisca-core @@ -52,7 +51,7 @@ outputs: host: - python run: - - python >=3.9,<4.0 + - python {% for req in data['extras_require']['web-api'] %} - {{ req }} {% endfor %} @@ -65,9 +64,7 @@ outputs: host: - python run: - - python >=3.9,<4.0 - - openfisca-country-template - - openfisca-extension-template + - python {% for req in data['extras_require']['dev'] %} - {{ req }} {% endfor %} diff --git a/.conda/openfisca-country-template/recipe.yaml b/.conda/openfisca-country-template/recipe.yaml index c2feb7cc14..7b75cf22c2 100644 --- a/.conda/openfisca-country-template/recipe.yaml +++ b/.conda/openfisca-country-template/recipe.yaml @@ -18,20 +18,19 @@ build: requirements: host: - - python + - numpy - pip + - python + - setuptools >=61.0 run: + - numpy - python + - openfisca-core >=42,<43 tests: - python: imports: - openfisca_country_template -- requirements: - run: - - pip - script: - - pip check about: summary: OpenFisca Rules as Code model for Country-Template. diff --git a/.conda/openfisca-country-template/variants.yaml b/.conda/openfisca-country-template/variants.yaml index 15259d5160..2a74a5018c 100644 --- a/.conda/openfisca-country-template/variants.yaml +++ b/.conda/openfisca-country-template/variants.yaml @@ -1,4 +1,5 @@ +numpy: +- "1.26" + python: -- 3.9 -- 3.10 -- 3.11 +- "3.10" diff --git a/.conda/openfisca-extension-template/recipe.yaml b/.conda/openfisca-extension-template/recipe.yaml index e4a0ab6912..03e53d5dd0 100644 --- a/.conda/openfisca-extension-template/recipe.yaml +++ b/.conda/openfisca-extension-template/recipe.yaml @@ -18,21 +18,19 @@ build: requirements: host: + - numpy + - pip - python - setuptools >=61.0 - - pip run: + - numpy - python + - openfisca-country-template >=7,<8 tests: - python: imports: - openfisca_extension_template -- requirements: - run: - - pip - script: - - pip check about: summary: An OpenFisca extension that adds some variables to an already-existing diff --git a/.conda/openfisca-extension-template/variants.yaml b/.conda/openfisca-extension-template/variants.yaml index 15259d5160..2a74a5018c 100644 --- a/.conda/openfisca-extension-template/variants.yaml +++ b/.conda/openfisca-extension-template/variants.yaml @@ -1,4 +1,5 @@ +numpy: +- "1.26" + python: -- 3.9 -- 3.10 -- 3.11 +- "3.10" diff --git a/.conda/pylint-per-file-ignores/recipe.yaml b/.conda/pylint-per-file-ignores/recipe.yaml index 05befd9567..4a573982f8 100644 --- a/.conda/pylint-per-file-ignores/recipe.yaml +++ b/.conda/pylint-per-file-ignores/recipe.yaml @@ -30,11 +30,6 @@ tests: - python: imports: - pylint_per_file_ignores -- requirements: - run: - - pip - script: - - pip check about: summary: A pylint plugin to ignore error codes per file. diff --git a/.conda/pylint-per-file-ignores/variants.yaml b/.conda/pylint-per-file-ignores/variants.yaml index 15259d5160..29b43111ce 100644 --- a/.conda/pylint-per-file-ignores/variants.yaml +++ b/.conda/pylint-per-file-ignores/variants.yaml @@ -1,4 +1,2 @@ python: -- 3.9 -- 3.10 -- 3.11 +- "3.10" diff --git a/.github/workflows/_before-conda.yaml b/.github/workflows/_before-conda.yaml index f202a1eb35..5fd92500a9 100644 --- a/.github/workflows/_before-conda.yaml +++ b/.github/workflows/_before-conda.yaml @@ -59,40 +59,27 @@ jobs: uses: conda-incubator/setup-miniconda@v3 with: activate-environment: openfisca - auto-update-conda: true - channels: conda-forge + miniforge-version: latest python-version: ${{ inputs.python }} + use-mamba: true if: steps.cache-env.outputs.cache-hit != 'true' - name: Install dependencies run: | - conda install conda-build conda-verify rattler-build anaconda-client - conda install numpy=${{ inputs.numpy }} - conda install python=${{ inputs.python }} + mamba install boa rattler-build anaconda-client + mamba install numpy=${{ inputs.numpy }} + mamba install python=${{ inputs.python }} if: steps.cache-env.outputs.cache-hit != 'true' - name: Update conda & dependencies uses: conda-incubator/setup-miniconda@v3 with: activate-environment: openfisca - auto-update-conda: true environment-file: .env.yaml + miniforge-version: latest + use-mamba: true if: steps.cache-env.outputs.cache-hit == 'true' - - name: Build country template package - run: | - rattler-build build \ - --recipe .conda/openfisca-country-template \ - --output-dir ~/conda-rel \ - --no-test - - - name: Build extension template package - run: | - rattler-build build \ - --recipe .conda/openfisca-extension-template \ - --output-dir ~/conda-rel \ - --no-test - - name: Build pylint plugin package run: | rattler-build build \ @@ -101,13 +88,24 @@ jobs: - name: Build core package run: | - mkdir -p ~/conda-rel - conda build .conda/openfisca-core \ + conda mambabuild .conda/openfisca-core \ --use-local \ --no-anaconda-upload \ --output-folder ~/conda-rel \ --numpy ${{ inputs.numpy }} \ --python ${{ inputs.python }} + - name: Build country template package + run: | + rattler-build build \ + --recipe .conda/openfisca-country-template \ + --output-dir ~/conda-rel \ + + - name: Build extension template package + run: | + rattler-build build \ + --recipe .conda/openfisca-extension-template \ + --output-dir ~/conda-rel + - name: Export env - run: conda env export --name openfisca > .env.yaml + run: mamba env export --name openfisca > .env.yaml diff --git a/.github/workflows/_test-conda.yaml b/.github/workflows/_test-conda.yaml index e1a6986740..fab88ac1df 100644 --- a/.github/workflows/_test-conda.yaml +++ b/.github/workflows/_test-conda.yaml @@ -55,11 +55,16 @@ jobs: uses: conda-incubator/setup-miniconda@v3 with: activate-environment: openfisca - auto-update-conda: true environment-file: .env.yaml + miniforge-version: latest + use-mamba: true - - name: Install core - run: conda install openfisca-core-dev --channel file:///home/runner/conda-rel + - name: Install packages + run: | + mamba install --channel file:///home/runner/conda-rel \ + openfisca-core-dev \ + openfisca-country-template \ + openfisca-extension-template - name: Run core tests run: make test-core diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index f6753b400f..4c15fb54f9 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -173,6 +173,9 @@ jobs: publish-to-conda: runs-on: ubuntu-22.04 needs: [publish-to-pypi] + defaults: + run: + shell: bash -l {0} steps: - name: Checkout @@ -203,23 +206,12 @@ jobs: uses: conda-incubator/setup-miniconda@v3 with: activate-environment: openfisca - auto-update-conda: true environment-file: .env.yaml + miniforge-version: latest + use-mamba: true - name: Publish to conda run: | - anaconda upload ~/conda-rel/noarch/openfisca-extension-template-* \ - --token ${{ secrets.ANACONDA_TOKEN }} - --user openfisca - --force - anaconda upload ~/conda-rel/noarch/openfisca-country-template-* \ - --token ${{ secrets.ANACONDA_TOKEN }} - --user openfisca - --force - anaconda upload ~/conda-rel/noarch/pylint-per-file-ignores-* \ - --token ${{ secrets.ANACONDA_TOKEN }} - --user openfisca - --force anaconda upload ~/conda-rel/noarch/openfisca-core-* \ --token ${{ secrets.ANACONDA_TOKEN }} --user openfisca @@ -228,6 +220,9 @@ jobs: test-on-windows: runs-on: windows-2019 needs: [publish-to-conda] + defaults: + run: + shell: bash -l {0} steps: - uses: conda-incubator/setup-miniconda@v3 diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index 2a8fbdef76..7bee48c81c 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -17,7 +17,7 @@ jobs: numpy: [1.26.4, 1.24.2] # Patch version must be specified to avoid any cache confusion, since # the cache key depends on the full Python version. If left unspecified, - # different patch versions could be allocated between jobs, and any + # different patch versions could be allocated between jobs, and any # such difference would lead to a cache not found error. python: [3.11.9, 3.9.13] include: From b9efbafa3e47fe1fcbcb6161b9ecc9e9ba35d9e4 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 3 Oct 2024 23:50:47 +0200 Subject: [PATCH 185/188] ci: use mamba --- .conda/openfisca-core/meta.yaml | 5 +++++ .conda/openfisca-country-template/variants.yaml | 2 ++ .conda/openfisca-extension-template/variants.yaml | 2 ++ .conda/pylint-per-file-ignores/variants.yaml | 2 ++ .github/workflows/_before-conda.yaml | 8 +++----- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.conda/openfisca-core/meta.yaml b/.conda/openfisca-core/meta.yaml index 88df5d411d..be31e84b95 100644 --- a/.conda/openfisca-core/meta.yaml +++ b/.conda/openfisca-core/meta.yaml @@ -24,6 +24,7 @@ build: requirements: host: + - numpy - pip - python - setuptools >=61.0 @@ -49,8 +50,10 @@ outputs: noarch: python requirements: host: + - numpy - python run: + - numpy - python {% for req in data['extras_require']['web-api'] %} - {{ req }} @@ -62,8 +65,10 @@ outputs: noarch: python requirements: host: + - numpy - python run: + - numpy - python {% for req in data['extras_require']['dev'] %} - {{ req }} diff --git a/.conda/openfisca-country-template/variants.yaml b/.conda/openfisca-country-template/variants.yaml index 2a74a5018c..64e0aaf0f1 100644 --- a/.conda/openfisca-country-template/variants.yaml +++ b/.conda/openfisca-country-template/variants.yaml @@ -2,4 +2,6 @@ numpy: - "1.26" python: +- "3.9" - "3.10" +- "3.11" diff --git a/.conda/openfisca-extension-template/variants.yaml b/.conda/openfisca-extension-template/variants.yaml index 2a74a5018c..64e0aaf0f1 100644 --- a/.conda/openfisca-extension-template/variants.yaml +++ b/.conda/openfisca-extension-template/variants.yaml @@ -2,4 +2,6 @@ numpy: - "1.26" python: +- "3.9" - "3.10" +- "3.11" diff --git a/.conda/pylint-per-file-ignores/variants.yaml b/.conda/pylint-per-file-ignores/variants.yaml index 29b43111ce..ab419e422e 100644 --- a/.conda/pylint-per-file-ignores/variants.yaml +++ b/.conda/pylint-per-file-ignores/variants.yaml @@ -1,2 +1,4 @@ python: +- "3.9" - "3.10" +- "3.11" diff --git a/.github/workflows/_before-conda.yaml b/.github/workflows/_before-conda.yaml index 5fd92500a9..7528a6a1c2 100644 --- a/.github/workflows/_before-conda.yaml +++ b/.github/workflows/_before-conda.yaml @@ -40,6 +40,7 @@ jobs: .env.yaml key: conda-env-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} restore-keys: conda-env-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- + id: cache-env - name: Cache conda deps uses: actions/cache@v4 @@ -47,7 +48,7 @@ jobs: path: ~/conda_pkgs_dir key: conda-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}-${{ hashFiles('setup.py') }} restore-keys: conda-deps-${{ inputs.os }}-np${{ inputs.numpy }}-py${{ inputs.python }}- - id: cache-env + id: cache-deps - name: Cache release uses: actions/cache@v4 @@ -65,10 +66,7 @@ jobs: if: steps.cache-env.outputs.cache-hit != 'true' - name: Install dependencies - run: | - mamba install boa rattler-build anaconda-client - mamba install numpy=${{ inputs.numpy }} - mamba install python=${{ inputs.python }} + run: mamba install boa rattler-build anaconda-client if: steps.cache-env.outputs.cache-hit != 'true' - name: Update conda & dependencies From 8c5956e250c7358d602ff0f2b79d1a0a8a2440ba Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 4 Oct 2024 01:02:49 +0200 Subject: [PATCH 186/188] ci: do not rename check-version --- .github/workflows/_version.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/_version.yaml b/.github/workflows/_version.yaml index dcb82a1509..27c4737a4f 100644 --- a/.github/workflows/_version.yaml +++ b/.github/workflows/_version.yaml @@ -18,7 +18,6 @@ jobs: # during the time they took to fix the tests. check-version: runs-on: ${{ inputs.os }} - name: check-version-${{ inputs.os }}-py${{ inputs.python }} env: # To colorize output of make tasks. TERM: xterm-256color From a02fca799b53e2a9a278980388762d98b789791a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 4 Oct 2024 15:55:59 +0200 Subject: [PATCH 187/188] docs: keep README in .conda --- .conda/{openfisca-core => }/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .conda/{openfisca-core => }/README.md (100%) diff --git a/.conda/openfisca-core/README.md b/.conda/README.md similarity index 100% rename from .conda/openfisca-core/README.md rename to .conda/README.md From 7fddb1823ed7665a86e684866afa1f66fa238c8e Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 3 Oct 2024 18:03:17 +0200 Subject: [PATCH 188/188] chore: version bump --- .github/workflows/merge.yaml | 3 +++ CHANGELOG.md | 18 ++++++++++++++++++ setup.py | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index 4c15fb54f9..5c2b4c791d 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -211,6 +211,7 @@ jobs: use-mamba: true - name: Publish to conda + shell: bash -l {0} run: | anaconda upload ~/conda-rel/noarch/openfisca-core-* \ --token ${{ secrets.ANACONDA_TOKEN }} @@ -238,7 +239,9 @@ jobs: uses: actions/checkout@v4 - name: Install with conda + shell: bash -l {0} run: conda install -c openfisca openfisca-core - name: Test openfisca + shell: bash -l {0} run: openfisca --help diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b914428e..bf2962fd85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +### 42.0.4 [#1257](https://github.com/openfisca/openfisca-core/pull/1257) + +#### Technical changes + +- Fix conda test and publish +- Add matrix testing to CI + - Now it tests lower and upper bounds of python and numpy versions + +### 42.0.3 [#1234](https://github.com/openfisca/openfisca-core/pull/1234) + +#### Technical changes + +- Add matrix testing to CI + - Now it tests lower and upper bounds of python and numpy versions + +> Note: Version `42.0.3` has been unpublished as was deployed by mistake. +> Please use versions `42.0.4` and subsequents. + ### 42.0.2 [#1256](https://github.com/openfisca/openfisca-core/pull/1256) #### Documentation diff --git a/setup.py b/setup.py index 7e5ed62c23..fcfe490269 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( name="OpenFisca-Core", - version="42.0.2", + version="42.0.4", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[