From 22d6e635ecd0f28a94fbebe9090e323739ad81d7 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 12:40:13 +0200 Subject: [PATCH 01/39] Add checks to entities --- openfisca_tasks/lint.mk | 2 ++ setup.cfg | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 115c6267bb..4ab78156b2 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -17,6 +17,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 \ ; @@ -42,6 +43,7 @@ check-types: ## Run static type checkers for type errors (strict). lint-typing-strict: \ lint-typing-strict-commons \ + lint-typing-strict-entities \ lint-typing-strict-types \ ; diff --git a/setup.cfg b/setup.cfg index bb3ff50fc5..27047ba352 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ extend-ignore = D hang-closing = true ignore = E128,E251,F403,F405,E501,RST301,W503,W504 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/types +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/types rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, obj strictness = short @@ -41,7 +41,7 @@ skip_empty = true addopts = --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py -testpaths = openfisca_core/commons openfisca_core/types tests +testpaths = openfisca_core/commons openfisca_core/entities openfisca_core/types tests [mypy] ignore_missing_imports = True From 9e330faa5ead2ae39b3ae3669d11e732b0258ba7 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 12:43:01 +0200 Subject: [PATCH 02/39] Make entity a dataclass --- openfisca_core/entities/entity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index c0da47f9bc..5eacb80b98 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import os import textwrap +from dataclasses import dataclass from openfisca_core.entities import Role +@dataclass class Entity: """ Represents an entity (e.g. a person, a household, etc.) on which calculations can be run. From 959fc7c8d83e9f7a378bc6829f1fad1c3e59875e Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 12:51:16 +0200 Subject: [PATCH 03/39] Add docs to Entity --- openfisca_core/entities/entity.py | 43 +++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 5eacb80b98..daa10dacd5 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -9,8 +9,47 @@ @dataclass class Entity: - """ - Represents an entity (e.g. a person, a household, etc.) on which calculations can be run. + """Represents an entity on which calculations can be run. + + For example an individual, a company, etc. An :class:`.Entity` + represents an "abstract" atomic unit of the legislation, as in + "any individual", or "any company". + + Attributes: + key: Key to identify the :class:`.Entity`. + plural: The ``key``, pluralised. + label: A summary description. + doc: A full description, dedented. + is_person: Represents an individual. Defaults to True. + + Args: + key: Key to identify the :class:`.Entity`. + plural: ``key``, pluralised. + label: A summary description. + doc: A full description. + + Examples: + >>> entity = Entity( + ... "individual", + ... "individuals", + ... "An individual", + ... "The minimal legal entity on which a rule might be applied.", + ... ) + + >>> repr(Entity) + "" + + >>> repr(entity) + '' + + >>> str(entity) + 'individuals' + + .. versionchanged:: 35.7.0 + * Added documentation, doctests, and typing. + * Transformed into a :func:`dataclasses.dataclass`. + * Added :attr:`object.__slots__` to improve performance. + """ def __init__(self, key, plural, label, doc): From 5a8be59a143542e2a45ad91fd937e41a32d26522 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 12:55:40 +0200 Subject: [PATCH 04/39] Add slots to Entity --- openfisca_core/entities/entity.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index daa10dacd5..718af6cbb3 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -52,6 +52,15 @@ class Entity: """ + __slots__ = tuple(( + "key", + "plural", + "label", + "doc", + "is_person", + "_tax_benefit_system", + )) + def __init__(self, key, plural, label, doc): self.key = key self.label = label From 77eb6e1064be6b16d905af5fe69567f048031990 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 12:56:58 +0200 Subject: [PATCH 05/39] Add typed attributes to Entity --- openfisca_core/entities/entity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 718af6cbb3..3ebd04a1ae 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -61,6 +61,11 @@ class Entity: "_tax_benefit_system", )) + key: str + plural: str + label: str + doc: str + def __init__(self, key, plural, label, doc): self.key = key self.label = label From 74e62bb3bb6941cfc8a395e96fde589be555f067 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 13:00:19 +0200 Subject: [PATCH 06/39] Refactor __init__ in Entity --- openfisca_core/entities/entity.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 3ebd04a1ae..2d5df3780e 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import os import textwrap from dataclasses import dataclass @@ -66,13 +68,9 @@ class Entity: label: str doc: str - def __init__(self, key, plural, label, doc): - self.key = key - self.label = label - self.plural = plural - self.doc = textwrap.dedent(doc) + def __post_init__(self, *_args: Any) -> None: + self.doc = textwrap.dedent(self.doc) self.is_person = True - self._tax_benefit_system = None def set_tax_benefit_system(self, tax_benefit_system): self._tax_benefit_system = tax_benefit_system From 69368011186839dc3f5040ca0db374350d6aa0e1 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 13:02:25 +0200 Subject: [PATCH 07/39] Fix repr in Entity --- openfisca_core/entities/entity.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 2d5df3780e..6cfdc6f391 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -42,7 +42,7 @@ class Entity: "" >>> repr(entity) - '' + 'Entity(individual)' >>> str(entity) 'individuals' @@ -72,6 +72,9 @@ def __post_init__(self, *_args: Any) -> None: self.doc = textwrap.dedent(self.doc) self.is_person = True + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.key})" + def set_tax_benefit_system(self, tax_benefit_system): self._tax_benefit_system = tax_benefit_system From 9d3a7228d6ec39b003a0b02eca352b25f0aca0fd Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 13:03:50 +0200 Subject: [PATCH 08/39] Fix str in Entity --- openfisca_core/entities/entity.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 6cfdc6f391..17b28fc232 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -75,6 +75,9 @@ def __post_init__(self, *_args: Any) -> None: def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key})" + def __str__(self) -> str: + return self.plural + def set_tax_benefit_system(self, tax_benefit_system): self._tax_benefit_system = tax_benefit_system From 6a739f19ae98194209f67ebab0c7d88fc91c4777 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 13:07:19 +0200 Subject: [PATCH 09/39] Make Entity.check_role_validity --- openfisca_core/entities/entity.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 17b28fc232..b4198de6f2 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -81,7 +81,8 @@ def __str__(self) -> str: def set_tax_benefit_system(self, tax_benefit_system): self._tax_benefit_system = tax_benefit_system - def check_role_validity(self, role): + @staticmethod + def check_role_validity(role): if role is not None and not type(role) == Role: raise ValueError("{} is not a valid role".format(role)) From 3b70c41d305b5355124e8b5df87ced1907e8f8b6 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 13:17:04 +0200 Subject: [PATCH 10/39] Add HasVariables protocol --- openfisca_core/types/__init__.py | 11 +++++++++-- .../{data_types/arrays.py => _data_types.py} | 4 ++-- openfisca_core/types/_protocols.py | 16 ++++++++++++++++ openfisca_core/types/data_types/__init__.py | 1 - setup.cfg | 2 +- 5 files changed, 28 insertions(+), 6 deletions(-) rename openfisca_core/types/{data_types/arrays.py => _data_types.py} (100%) create mode 100644 openfisca_core/types/_protocols.py delete mode 100644 openfisca_core/types/data_types/__init__.py diff --git a/openfisca_core/types/__init__.py b/openfisca_core/types/__init__.py index e14cfea65d..45c60a4f1a 100644 --- a/openfisca_core/types/__init__.py +++ b/openfisca_core/types/__init__.py @@ -5,8 +5,9 @@ and expected behaviours. Official Public API: - * ``ArrayLike`` + * :data:`.ArrayLike` * :attr:`.ArrayType` + * :class:`.HasVariables` Note: How imports are being used today:: @@ -37,9 +38,15 @@ # Official Public API -from .data_types import ( # noqa: F401 +from ._data_types import ( # noqa: F401 ArrayLike, ArrayType, ) __all__ = ["ArrayLike", "ArrayType"] + +from ._protocols import ( # noqa: F401 + HasVariables, + ) + +__all__ = ["HasVariables", *__all__] diff --git a/openfisca_core/types/data_types/arrays.py b/openfisca_core/types/_data_types.py similarity index 100% rename from openfisca_core/types/data_types/arrays.py rename to openfisca_core/types/_data_types.py index 5cfef639c5..07b4a036f8 100644 --- a/openfisca_core/types/data_types/arrays.py +++ b/openfisca_core/types/_data_types.py @@ -34,11 +34,11 @@ Todo: * Refactor once numpy version >= 1.21 is used. -.. versionadded:: 35.5.0 - .. versionchanged:: 35.6.0 Moved to :mod:`.types` +.. versionadded:: 35.5.0 + .. _mypy: https://mypy.readthedocs.io/en/stable/ diff --git a/openfisca_core/types/_protocols.py b/openfisca_core/types/_protocols.py new file mode 100644 index 0000000000..49b5a72237 --- /dev/null +++ b/openfisca_core/types/_protocols.py @@ -0,0 +1,16 @@ +import abc +from typing import Any, Optional + +from typing_extensions import Protocol + + +class HasVariables(Protocol): + """Duck-type for tax-benefit systems. + + .. versionadded:: 35.7.0 + + """ + + @abc.abstractmethod + def get_variable(self, __arg1: str, __arg2: bool = False) -> Optional[Any]: + """A tax-benefit system implements :meth:`.get_variable`.""" diff --git a/openfisca_core/types/data_types/__init__.py b/openfisca_core/types/data_types/__init__.py deleted file mode 100644 index 6dd38194e3..0000000000 --- a/openfisca_core/types/data_types/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .arrays import ArrayLike, ArrayType # noqa: F401 diff --git a/setup.cfg b/setup.cfg index 27047ba352..cdecaae600 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ ignore = E128,E251,F403,F405,E501,RST301,W503,W504 in-place = true include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/types rst-directives = attribute, deprecated, seealso, versionadded, versionchanged -rst-roles = any, attr, class, exc, func, meth, obj +rst-roles = any, attr, class, data, exc, func, meth, obj strictness = short [pylint.message_control] From ad51508d7abb5f680b4c99a18ccb472d5bc1d5b4 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 13:24:02 +0200 Subject: [PATCH 11/39] Add missing tbs property to Entity --- openfisca_core/entities/entity.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index b4198de6f2..c7f9235396 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Any +from typing import Any, Optional import os import textwrap from dataclasses import dataclass +from openfisca_core.types import HasVariables + from openfisca_core.entities import Role @@ -48,9 +50,7 @@ class Entity: 'individuals' .. versionchanged:: 35.7.0 - * Added documentation, doctests, and typing. - * Transformed into a :func:`dataclasses.dataclass`. - * Added :attr:`object.__slots__` to improve performance. + Added documentation, doctests, and typing. """ @@ -78,6 +78,16 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.plural + @property + def tax_benefit_system(self) -> Optional[HasVariables]: + """An :obj:`.Entity` belongs to a :obj:`.TaxBenefitSystem`.""" + + return self._tax_benefit_system + + @tax_benefit_system.setter + def tax_benefit_system(self, value: HasVariables) -> None: + self._tax_benefit_system = value + def set_tax_benefit_system(self, tax_benefit_system): self._tax_benefit_system = tax_benefit_system From 447dc9bdf822ba1b8b4b9190fd922d2fef87de08 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 13:31:20 +0200 Subject: [PATCH 12/39] Deprecate Entity.set_tax_benefit_system --- openfisca_core/entities/entity.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index c7f9235396..51092fccda 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -88,8 +88,20 @@ def tax_benefit_system(self) -> Optional[HasVariables]: def tax_benefit_system(self, value: HasVariables) -> None: self._tax_benefit_system = value - def set_tax_benefit_system(self, tax_benefit_system): - self._tax_benefit_system = tax_benefit_system + def set_tax_benefit_system(self, tax_benefit_system: HasVariables) -> None: + """Sets ``_tax_benefit_system``. + + Args: + tax_benefit_system: To query variables from. + + .. deprecated:: 35.7.0 + :meth:`.set_tax_benefit_system` has been deprecated and will be + removed in the future. The functionality is now provided by + :attr:`.tax_benefit_system`. + + """ + + self.tax_benefit_system = tax_benefit_system @staticmethod def check_role_validity(role): From 5c7b4ca11bcd0fa6858b0ce616086bb226e838f3 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 13:47:56 +0200 Subject: [PATCH 13/39] Move Entity,check_role_validity to helpers --- openfisca_core/commons/__init__.py | 6 +- openfisca_core/entities/__init__.py | 113 ++++++++++++++++++++++------ openfisca_core/entities/entity.py | 22 +++++- openfisca_core/entities/helpers.py | 36 ++++++++- 4 files changed, 142 insertions(+), 35 deletions(-) diff --git a/openfisca_core/commons/__init__.py b/openfisca_core/commons/__init__.py index b3b5d8cbb2..241a3bcbec 100644 --- a/openfisca_core/commons/__init__.py +++ b/openfisca_core/commons/__init__.py @@ -16,8 +16,8 @@ * :class:`.Dummy` Note: - The ``deprecated`` imports are transitional, in order to ensure non-breaking - changes, and could be removed from the codebase in the next + The ``deprecated`` imports are transitional, in order to ensure + non-breaking changes, and could be removed from the codebase in the next major release. Note: @@ -32,7 +32,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/entities/__init__.py b/openfisca_core/entities/__init__.py index 15b38e2a5c..a6aa21268d 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -1,27 +1,92 @@ -# Transitional imports to ensure non-breaking changes. -# Could be deprecated in the next major release. -# -# How imports are being used today: -# -# >>> from openfisca_core.module import symbol -# -# The previous example provokes cyclic dependency problems -# that prevent us from modularizing the different components -# of the library so to make them easier to test and to maintain. -# -# How could them be used after the next major release: -# -# >>> from openfisca_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from openfisca_core import module -# >>> module.Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - -from .helpers import build_entity # noqa: F401 +"""Provides a way of representing the entities of a rule system. + +Each rule system is comprised by legislation and regulations to be applied upon +"someone". In legal and economical terms, "someone" is referred to as people: +individuals, families, tax households, companies, and so on. + +People can be either human or non-human, that is a legal entity, also referred +to as a legal person. Human or non-human, a person is an atomic element of a +rule system: for example, in most legislations, a salary is invariably owed +to an indivual, and payroll taxes by a company, as a juridical person. In +OpenFisca, that atomic element is represented as an :class:`.Entity`. + +In other cases, legal and regulatory rules are defined for groups or clusters +of people: for example, income tax is usually due by a tax household, that is +a group of individuals. There may also be fiduciary entities where the members, +legal entities, are collectively liable for a property tax. In OpenFisca, those +cluster elements are represented as a :class:`.GroupEntity`. + +In the latter case, the atomic members of a given group may have a different +:class:`Role` in the context of a specific rule: for example, income tax +is due, in some legislations, by a tax household, where we find different +roles as the declarant, the spouse, the children, and so on… + +What's important is that each rule, or in OpenFisca, a :class:`.Variable`, +is defined either for an :class:`.Entity` or for a :class:`.GroupEntity`, +and in the latter case, the way the rule is going to be applied depends +on the attributes and roles of the members of the group. + +Finally, there is a distiction to be made between the "abstract" entities +described by in a rule system, for example an individual, as in "any" +individual, and an actual individual, like Mauko, Andrea, Mehdi, Seiko, +or José. + +This module provides tools for modelling the former. For the actual +"simulation" or "application" of any given :class:`.Variable` to a +concrete individual or group of individuals, see :class:`.Population` +and :class:`.GroupPopulation`. + +Official Public API: + * :class:`.Entity` + * :class:`.GroupEntity` + * :class:`.Role` + * :func:`.build_entity` + * :func:`.check_role_validity` + +Deprecated: + * :meth:`.Entity.set_tax_benefit_system` + * :meth:`.Entity.check_role_validity` + +Note: + The ``deprecated`` imports are transitional, in order to ensure + non-breaking changes, and could be removed from the codebase in the next + major release. + +Note: + How imports are being used today:: + + from openfisca_core.entities import * # Bad + from openfisca_core.entities.helpers import build_entity # Bad + from openfisca_core.entities.role import Role # Bad + + The previous examples provoke cyclic dependency problems, that prevent us + from modularizing the different components of the library, which would make + them easier to test and to maintain. + + How they could be used in a future release:: + + from openfisca_core import entities + from openfisca_core.entities import Role + + Role() # Good: import classes as publicly exposed + entities.build_entity() # Good: use functions as publicly exposed + + .. seealso:: `PEP8#Imports`_ and `OpenFisca's Styleguide`_. + + .. _PEP8#Imports: + https://www.python.org/dev/peps/pep-0008/#imports + + .. _OpenFisca's Styleguide: + https://github.com/openfisca/openfisca-core/blob/master/STYLEGUIDE.md + +""" + +# Official Public API + from .role import Role # noqa: F401 from .entity import Entity # noqa: F401 from .group_entity import GroupEntity # noqa: F401 +from .helpers import build_entity, check_role_validity # noqa: F401 + +__all__ = ["Entity", "GroupEntity", "Role"] +__all__ = ["build_entity", "check_role_validity", *__all__] diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 51092fccda..2c0c61aa89 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -8,7 +8,7 @@ from openfisca_core.types import HasVariables -from openfisca_core.entities import Role +from .. import entities @dataclass @@ -104,9 +104,23 @@ def set_tax_benefit_system(self, tax_benefit_system: HasVariables) -> None: self.tax_benefit_system = tax_benefit_system @staticmethod - def check_role_validity(role): - if role is not None and not type(role) == Role: - raise ValueError("{} is not a valid role".format(role)) + def check_role_validity(role: Any) -> None: + """Checks if ``role`` is an instance of :class:`.Role`. + + Args: + role: Any object. + + Returns: + None. + + .. deprecated:: 35.7.0 + :meth:`.check_role_validity` has been deprecated and will be + removed in the future. The functionality is now provided by + :func:`.entities.check_role_validity`. + + """ + + return entities.check_role_validity(role) def get_variable(self, variable_name, check_existence = False): return self._tax_benefit_system.get_variable(variable_name, check_existence) diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 86d7bb6a6b..55d553df19 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,8 +1,36 @@ -from openfisca_core import entities +from __future__ import annotations +from typing import Any -def build_entity(key, plural, label, doc = "", roles = None, is_person = False, class_override = None, containing_entities = ()): +from .role import Role +from .entity import Entity +from .group_entity import GroupEntity + + +def build_entity(key, plural, label, doc = "", roles = None, is_person = False, containing_entities = (), class_override = None): if is_person: - return entities.Entity(key, plural, label, doc) + return Entity(key, plural, label, doc, containing_entities) else: - return entities.GroupEntity(key, plural, label, doc, roles, containing_entities = containing_entities) + return GroupEntity(key, plural, label, doc, roles) + + +def check_role_validity(role: Any) -> None: + """Checks if ``role`` is an instance of :class:`.Role`. + + Args: + role: Any object. + + Raises: + ValueError: When ``role`` is not a :class:`.Role`. + + Examples: + >>> from openfisca_core.entities import Role + >>> role = Role({"key": "key"}, object()) + >>> check_role_validity(role) + + .. versionadded:: 35.7.0 + + """ + + if role is not None and not isinstance(role, Role): + raise ValueError(f"{role} is not a valid role") From ce9cb6b810ebaf8bf714e176b442b2db82e52629 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 14:09:24 +0200 Subject: [PATCH 14/39] Add docs to entities.build_entity --- openfisca_core/entities/entity.py | 6 +-- openfisca_core/entities/helpers.py | 72 +++++++++++++++++++++++++++--- openfisca_core/entities/role.py | 20 +++++++++ 3 files changed, 88 insertions(+), 10 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 2c0c61aa89..e110e454a3 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -49,7 +49,7 @@ class Entity: >>> str(entity) 'individuals' - .. versionchanged:: 35.7.0 + .. versionchanged:: 35.8.0 Added documentation, doctests, and typing. """ @@ -94,7 +94,7 @@ def set_tax_benefit_system(self, tax_benefit_system: HasVariables) -> None: Args: tax_benefit_system: To query variables from. - .. deprecated:: 35.7.0 + .. deprecated:: 35.8.0 :meth:`.set_tax_benefit_system` has been deprecated and will be removed in the future. The functionality is now provided by :attr:`.tax_benefit_system`. @@ -113,7 +113,7 @@ def check_role_validity(role: Any) -> None: Returns: None. - .. deprecated:: 35.7.0 + .. deprecated:: 35.8.0 :meth:`.check_role_validity` has been deprecated and will be removed in the future. The functionality is now provided by :func:`.entities.check_role_validity`. diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 55d553df19..24ea1af851 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,17 +1,75 @@ from __future__ import annotations -from typing import Any +from typing import Any, Optional, Sequence, Union -from .role import Role +from .role import Role, RoleLike from .entity import Entity from .group_entity import GroupEntity -def build_entity(key, plural, label, doc = "", roles = None, is_person = False, containing_entities = (), class_override = None): +def build_entity( + key: str, + plural: str, + label: str, + doc: str = "", + roles: Optional[Sequence[RoleLike]] = None, + is_person: bool = False, + containing_entities: Sequence[str] = (), + class_override: Optional[Any] = None, + ) -> Union[Entity, GroupEntity]: + """Builds an :class:`.Entity` or a :class:`.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: ? + + 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 iterable. + + Examples: + >>> build_entity( + ... "syndicate", + ... "syndicates", + ... "Banks loaning jointly.", + ... roles = [], + ... ) + GroupEntity(syndicate) + + >>> build_entity( + ... "company", + ... "companies", + ... "A small or medium company.", + ... is_person = True, + ... ) + Entity(company) + + .. versionchanged:: 35.8.0 + Instead of raising :exc:`TypeError` when ``roles`` is None, it does + now raise :exc:`ValueError` when ``roles`` is not iterable. + + .. versionchanged:: 35.7.0 + Builder accepts a ``containing_entities`` attribute, that allows the + defining of group entities which entirely contain other group entities. + + """ + if is_person: - return Entity(key, plural, label, doc, containing_entities) - else: - return GroupEntity(key, plural, label, doc, roles) + return Entity(key, plural, label, doc) + + if isinstance(roles, (list, tuple)): + GroupEntity(key, plural, label, doc, roles, containing_entities) + + raise ValueError(f"Invalid value '{roles}' for 'roles', must be a list.") def check_role_validity(role: Any) -> None: @@ -28,7 +86,7 @@ def check_role_validity(role: Any) -> None: >>> role = Role({"key": "key"}, object()) >>> check_role_validity(role) - .. versionadded:: 35.7.0 + .. versionadded:: 35.8.0 """ diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index ea815ed513..6c1b556c6d 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,3 +1,8 @@ +from __future__ import annotations + +from typing import Optional, Sequence +from typing_extensions import TypedDict + import textwrap @@ -14,3 +19,18 @@ def __init__(self, description, entity): def __repr__(self): return "Role({})".format(self.key) + + +class RoleLike(TypedDict, total = False): + """Base type for any data castable to a :class:`.Role`. + + .. versionadded:: 35.7.0 + + """ + + key: str + plural: Optional[str] + label: Optional[str] + doc: Optional[str] + max: Optional[int] + subroles: Optional[Sequence[str]] From 6bc724642b0e3516e0f4f9fd453661e3ce88b473 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 15:04:03 +0200 Subject: [PATCH 15/39] Add docs to Role --- openfisca_core/entities/role.py | 71 +++++++++++++++++++++++++++--- openfisca_core/types/__init__.py | 6 ++- openfisca_core/types/_protocols.py | 25 ++++++++++- 3 files changed, 95 insertions(+), 7 deletions(-) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 6c1b556c6d..e35ee3b1e3 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -4,21 +4,82 @@ from typing_extensions import TypedDict import textwrap +from dataclasses import dataclass +from openfisca_core.types import HasRoles, SupportsRole + +@dataclass(init = False) class Role: + """Role of an :class:`.Entity` within a :class:`.GroupEntity`. + + Each :class:`.Entity` related to a :class:`.GroupEntity` has a + :class:`.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 so on. + + Attributes: + entity: Entity the :class:`.Role` belongs to. + key: Key to identify the :class:`.Role`. + plural: The ``key``, pluralised. + label: A summary description. + doc: A full description, dedented. + max: Max number of members. Defaults to None. + subroles: The ``subroles``. Defaults to None. + + Args: + description: A dictionary containing most of the attributes. + entity: :obj:`.Entity` the :class:`.Role` belongs to. + + Examples: + >>> description = { + ... "key": "parent", + ... "label": "Parents", + ... "plural": "parents", + ... "doc": "The one or two adults in charge of the household.", + ... "max": 2, + ... } + + >>> role = Role(description, object()) + + >>> repr(Role) + "" + + >>> repr(role) + 'Role(parent)' + + >>> str(role) + 'parent' + + .. versionchanged:: 35.7.0 + Added documentation, doctests, and typing. + + """ + + __slots__ = "entity", "key", "plural", "label", "doc", "max", "subroles" - def __init__(self, description, entity): + entity: HasRoles + key: str + plural: Optional[str] + label: Optional[str] + doc: Optional[str] + max: Optional[int] + subroles: Optional[Sequence[SupportsRole]] + + def __init__(self, description: RoleLike, entity: HasRoles) -> None: 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.label = description.get('label') + self.doc = textwrap.dedent(str(description.get('doc', ""))) self.max = description.get('max') self.subroles = None - def __repr__(self): - return "Role({})".format(self.key) + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.key})" + + def __str__(self) -> str: + return self.key class RoleLike(TypedDict, total = False): diff --git a/openfisca_core/types/__init__.py b/openfisca_core/types/__init__.py index 45c60a4f1a..04c07584d7 100644 --- a/openfisca_core/types/__init__.py +++ b/openfisca_core/types/__init__.py @@ -7,7 +7,9 @@ Official Public API: * :data:`.ArrayLike` * :attr:`.ArrayType` + * :class:`.HasRoles` * :class:`.HasVariables` + * :class:`.SupportsRole` Note: How imports are being used today:: @@ -46,7 +48,9 @@ __all__ = ["ArrayLike", "ArrayType"] from ._protocols import ( # noqa: F401 + HasRoles, HasVariables, + SupportsRole, ) -__all__ = ["HasVariables", *__all__] +__all__ = ["HasRoles", "HasVariables", "SupportsRole", *__all__] diff --git a/openfisca_core/types/_protocols.py b/openfisca_core/types/_protocols.py index 49b5a72237..ede4aa61dd 100644 --- a/openfisca_core/types/_protocols.py +++ b/openfisca_core/types/_protocols.py @@ -1,9 +1,21 @@ +from __future__ import annotations + import abc -from typing import Any, Optional +from typing import Any, Optional, Sequence from typing_extensions import Protocol +class HasRoles(Protocol): + """Duck-type for entities. + + .. versionadded:: 35.7.0 + + """ + + roles: Sequence[SupportsRole] + + class HasVariables(Protocol): """Duck-type for tax-benefit systems. @@ -14,3 +26,14 @@ class HasVariables(Protocol): @abc.abstractmethod def get_variable(self, __arg1: str, __arg2: bool = False) -> Optional[Any]: """A tax-benefit system implements :meth:`.get_variable`.""" + + +class SupportsRole(Protocol): + """Duck-type for roles. + + .. versionadded:: 35.7.0 + + """ + + max: Optional[int] + subroles: Optional[Sequence[SupportsRole]] From 6b0cdf6330a0435dbe3da2a29a733d8fb15b1ec9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 15:11:42 +0200 Subject: [PATCH 16/39] Make SupportsRole runtime-checkable --- openfisca_core/entities/helpers.py | 6 ++++-- openfisca_core/types/_protocols.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 24ea1af851..291af6d3ab 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -2,7 +2,9 @@ from typing import Any, Optional, Sequence, Union -from .role import Role, RoleLike +from openfisca_core.types import SupportsRole + +from .role import RoleLike from .entity import Entity from .group_entity import GroupEntity @@ -90,5 +92,5 @@ def check_role_validity(role: Any) -> None: """ - if role is not None and not isinstance(role, Role): + if role is not None and not isinstance(role, SupportsRole): raise ValueError(f"{role} is not a valid role") diff --git a/openfisca_core/types/_protocols.py b/openfisca_core/types/_protocols.py index ede4aa61dd..bdd061e9fd 100644 --- a/openfisca_core/types/_protocols.py +++ b/openfisca_core/types/_protocols.py @@ -3,7 +3,7 @@ import abc from typing import Any, Optional, Sequence -from typing_extensions import Protocol +from typing_extensions import Protocol, runtime_checkable class HasRoles(Protocol): @@ -28,6 +28,7 @@ def get_variable(self, __arg1: str, __arg2: bool = False) -> Optional[Any]: """A tax-benefit system implements :meth:`.get_variable`.""" +@runtime_checkable class SupportsRole(Protocol): """Duck-type for roles. From d076b396c0d23cf74df34568fd66185fa8baa97a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 15:20:56 +0200 Subject: [PATCH 17/39] Add doc to GroupEntity --- openfisca_core/commons/formulas.py | 4 +- openfisca_core/commons/misc.py | 5 +- openfisca_core/commons/rates.py | 5 +- openfisca_core/entities/entity.py | 8 +- openfisca_core/entities/group_entity.py | 253 ++++++++++++++++-- openfisca_core/entities/helpers.py | 24 +- openfisca_core/entities/role.py | 33 +-- openfisca_core/types/_protocols.py | 40 --- openfisca_core/{types => typing}/__init__.py | 29 +- openfisca_core/typing/_protocols.py | 62 +++++ openfisca_core/typing/_schemas.py | 19 ++ .../_data_types.py => typing/_types.py} | 7 +- openfisca_tasks/lint.mk | 4 +- setup.cfg | 4 +- setup.py | 2 +- 15 files changed, 379 insertions(+), 120 deletions(-) delete mode 100644 openfisca_core/types/_protocols.py rename openfisca_core/{types => typing}/__init__.py (61%) create mode 100644 openfisca_core/typing/_protocols.py create mode 100644 openfisca_core/typing/_schemas.py rename openfisca_core/{types/_data_types.py => typing/_types.py} (90%) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 6a90622147..86636f1bc1 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,8 +1,10 @@ +from __future__ import annotations + from typing import Any, Dict, Sequence, TypeVar +from openfisca_core.typing import ArrayLike, ArrayType import numpy -from openfisca_core.types import ArrayLike, ArrayType T = TypeVar("T") diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index dd05cea11b..2461b9afa2 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,6 +1,7 @@ -from typing import TypeVar +from __future__ import annotations -from openfisca_core.types import ArrayType +from typing import TypeVar +from openfisca_core.typing import ArrayType T = TypeVar("T") diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index d682824207..196f2072ea 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -1,9 +1,10 @@ +from __future__ import annotations + from typing import Optional +from openfisca_core.typing import ArrayLike, ArrayType import numpy -from openfisca_core.types import ArrayLike, ArrayType - def average_rate( target: ArrayType[float], diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index e110e454a3..ea233bb130 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,12 +1,12 @@ from __future__ import annotations from typing import Any, Optional +from openfisca_core.typing import TaxBenefitSystemProtocol import os import textwrap from dataclasses import dataclass -from openfisca_core.types import HasVariables from .. import entities @@ -79,16 +79,16 @@ def __str__(self) -> str: return self.plural @property - def tax_benefit_system(self) -> Optional[HasVariables]: + def tax_benefit_system(self) -> Optional[TaxBenefitSystemProtocol]: """An :obj:`.Entity` belongs to a :obj:`.TaxBenefitSystem`.""" return self._tax_benefit_system @tax_benefit_system.setter - def tax_benefit_system(self, value: HasVariables) -> None: + def tax_benefit_system(self, value: TaxBenefitSystemProtocol) -> None: self._tax_benefit_system = value - def set_tax_benefit_system(self, tax_benefit_system: HasVariables) -> None: + def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystemProtocol) -> None: """Sets ``_tax_benefit_system``. Args: diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index 0d58acc6ba..ee0ac48247 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,6 +1,16 @@ -from openfisca_core.entities import Entity, Role +from __future__ import annotations +from typing import Any, Dict, Iterable, Sequence +from openfisca_core.typing import RoleProtocol, RoleSchema +import dataclasses +from itertools import chain + +from .entity import Entity +from .role import Role + + +@dataclasses.dataclass class GroupEntity(Entity): """Represents an entity containing several others with different roles. @@ -8,14 +18,80 @@ class GroupEntity(Entity): several other :class:`.Entity` with different :class:`.Role`, and on which calculations can be run. + Attributes: + key: + Key to identify the :class:`.GroupEntity`. + plural: + The ``key``, pluralised. + label: + A summary description. + doc: + A full description, dedented. + is_person: + Represents an individual? Defaults to False. + roles: + List of the roles of the group entity. + flattened_roles: + ``roles`` flattened out. + containing_entities: + Keys of :obj:`.GroupEntity` whose members are guaranteed to be a + superset of this group's entities. + Args: - key: A key to identify the group entity. - plural: The ``key``, pluralised. - label: A summary description. - doc: A full description. - roles: The list of :class:`.Role` 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. + key: + Key to identify the :class:`.GroupEntity`. + plural: + The ``key``, pluralised. + label: + A summary description. + doc: + A full description, dedented. + roles: + The list of :class:`.Role` of the :class:`.GroupEntity`. + containing_entities: + The list of keys of :obj:`.GroupEntity` whose members are + guaranteed to be a superset of this group's entities. + + Examples: + >>> family_roles = [{ + ... "key": "parent", + ... "subroles": ["first_parent", "second_parent"], + ... }] + + >>> family = GroupEntity( + ... "family", + ... "families", + ... "A family", + ... "All the people somehow related living together.", + ... family_roles, + ... ) + + >>> household_roles = [{ + ... "key": "partners", + ... "subroles": ["first_partner", "second_partner"], + ... }] + + + >>> household = GroupEntity( + ... "household", + ... "households", + ... "A household", + ... "All the people who live together in the same place.", + ... household_roles, + ... (family.key,), + ... ) + + >>> repr(GroupEntity) + "" + + >>> repr(household) + 'GroupEntity(household)' + + >>> str(household) + 'households' + + .. versionchanged:: 35.8.0 + Added documentation, doctests, and typing. .. versionchanged:: 35.7.0 Added ``containing_entities``, that allows the defining of group @@ -23,21 +99,150 @@ class GroupEntity(Entity): """ - def __init__(self, key, plural, label, doc, roles, containing_entities = ()): + __slots__ = tuple(( + "_cached_roles", + "_tax_benefit_system", + "containing_entities", + "doc", + "flattened_roles", + "is_person", + "key", + "label", + "plural", + "roles", + "roles_description", + )) + + is_person: bool + roles: Sequence[RoleProtocol] + containing_entities: Sequence[str] + flattened_roles: Sequence[RoleProtocol] + roles_description: Sequence[RoleSchema] + _cached_roles: Dict[str, RoleProtocol] + + def __init__( + self, + key: str, + plural: str, + label: str, + doc: str, + roles: Sequence[RoleSchema], + containing_entities: Sequence[str] = (), + ): + super().__init__(key, plural, label, doc) - self.roles_description = roles - self.roles = [] - for role_description in roles: - role = Role(role_description, self) - setattr(self, role.key.upper(), role) - self.roles.append(role) - if role_description.get('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.max = len(role.subroles) - self.flattened_roles = sum([role2.subroles or [role2] for role2 in self.roles], []) - self.is_person = False self.containing_entities = containing_entities + self.roles = tuple(_build_role(self, desc) for desc in roles) + self.flattened_roles = _flatten_roles(self.roles) + self.roles_description = roles + self._cached_roles = _cache_roles((*self.roles, *self.flattened_roles)) + + def __getattr__(self, attr: str) -> Any: + if attr.isupper(): + role = self._cached_roles.get(attr) + + if role is not None: + return role + + return self.__getattribute__(attr) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.key})" + + def __str__(self) -> str: + return self.plural + + +def _build_role(entity: GroupEntity, description: RoleSchema) -> Role: + """Build roles & sub-roles. + + Args: + entity: A :obj:`.GroupEntity`. + description: A :obj:`dict` to build a :obj:`.Role`. + + Returns: + Role. + + Examples: + >>> description = {"key": "key"} + >>> _build_role(object(), description) + Role(key) + + >>> _build_role(object(), {}) + Traceback (most recent call last): + KeyError: 'key' + + .. versionadded:: 35.8.0 + + """ + + role = Role(description, entity) + subroles = description.get("subroles", ()) + + if subroles: + role.subroles = () + + for key in subroles: + subrole = Role({"key": key, "max": 1}, entity) + role.subroles = (*role.subroles, subrole) + + role.max = len(role.subroles) + + return role + + +def _flatten_roles(roles: Sequence[RoleProtocol]) -> Sequence[RoleProtocol]: + """Flattens roles' subroles to a sequence. + + Args: + roles: A list of :obj:`.Role`. + + Returns: + A list of subroles. + + Examples: + >>> _flatten_roles([]) + () + + >>> role = Role({"key": "key"}, object()) + >>> _flatten_roles([role]) + (Role(key),) + + >>> role.subroles = role, + >>> _flatten_roles([role]) + (Role(key),) + + >>> subrole = Role({"key": "llave"}, object()) + >>> role.subroles += subrole, + >>> _flatten_roles([role]) + (Role(key), Role(llave)) + + .. versionadded:: 35.8.0 + + """ + + tree: Iterable[Iterable[RoleProtocol]] + tree = [role.subroles or [role] for role in roles] + + return tuple(chain.from_iterable(tree)) + + +def _cache_roles(roles: Sequence[RoleProtocol]) -> Dict[str, RoleProtocol]: + """Create a cached dictionary of :obj:`.Role`. + + Args: + roles: The :obj:`.Role` to cache. + + Returns: + Dictionary with the cached :obj:`.Role`. + + Examples: + >>> role = Role({"key": "key"}, object()) + >>> _cache_roles([role]) + {'KEY': Role(key)} + + .. versionadded:: 35.8.0 + + """ + + return {role.key.upper(): role for role in roles} diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 291af6d3ab..0d9c93e024 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,10 +1,8 @@ from __future__ import annotations from typing import Any, Optional, Sequence, Union +from openfisca_core.typing import RoleProtocol, RoleSchema -from openfisca_core.types import SupportsRole - -from .role import RoleLike from .entity import Entity from .group_entity import GroupEntity @@ -14,10 +12,10 @@ def build_entity( plural: str, label: str, doc: str = "", - roles: Optional[Sequence[RoleLike]] = None, + roles: Optional[Sequence[RoleSchema]] = None, is_person: bool = False, - containing_entities: Sequence[str] = (), class_override: Optional[Any] = None, + containing_entities: Sequence[str] = (), ) -> Union[Entity, GroupEntity]: """Builds an :class:`.Entity` or a :class:`.GroupEntity`. @@ -29,6 +27,7 @@ def build_entity( 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`: @@ -44,6 +43,7 @@ def build_entity( ... "syndicates", ... "Banks loaning jointly.", ... roles = [], + ... containing_entities = (), ... ) GroupEntity(syndicate) @@ -55,6 +55,16 @@ def build_entity( ... ) Entity(company) + >>> role = Role({"key": "key"}, object()) + >>> build_entity( + ... "syndicate", + ... "syndicates", + ... "Banks loaning jointly.", + ... roles = role, + ... ) + Traceback (most recent call last): + ValueError: Invalid value 'key' for 'roles', must be a list. + .. versionchanged:: 35.8.0 Instead of raising :exc:`TypeError` when ``roles`` is None, it does now raise :exc:`ValueError` when ``roles`` is not iterable. @@ -69,7 +79,7 @@ def build_entity( return Entity(key, plural, label, doc) if isinstance(roles, (list, tuple)): - GroupEntity(key, plural, label, doc, roles, containing_entities) + return GroupEntity(key, plural, label, doc, roles, containing_entities) raise ValueError(f"Invalid value '{roles}' for 'roles', must be a list.") @@ -92,5 +102,5 @@ def check_role_validity(role: Any) -> None: """ - if role is not None and not isinstance(role, SupportsRole): + if role is not None and not isinstance(role, RoleProtocol): raise ValueError(f"{role} is not a valid role") diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index e35ee3b1e3..a3c28b5e7a 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,15 +1,13 @@ from __future__ import annotations from typing import Optional, Sequence -from typing_extensions import TypedDict +from openfisca_core.typing import GroupEntityProtocol, RoleProtocol, RoleSchema import textwrap from dataclasses import dataclass -from openfisca_core.types import HasRoles, SupportsRole - -@dataclass(init = False) +@dataclass class Role: """Role of an :class:`.Entity` within a :class:`.GroupEntity`. @@ -51,22 +49,26 @@ class Role: >>> str(role) 'parent' - .. versionchanged:: 35.7.0 + .. versionchanged:: 35.8.0 Added documentation, doctests, and typing. """ __slots__ = "entity", "key", "plural", "label", "doc", "max", "subroles" - entity: HasRoles + entity: GroupEntityProtocol key: str plural: Optional[str] label: Optional[str] doc: Optional[str] max: Optional[int] - subroles: Optional[Sequence[SupportsRole]] + subroles: Optional[Sequence[RoleProtocol]] - def __init__(self, description: RoleLike, entity: HasRoles) -> None: + def __init__( + self, + description: RoleSchema, + entity: GroupEntityProtocol, + ) -> None: self.entity = entity self.key = description['key'] self.plural = description.get('plural') @@ -80,18 +82,3 @@ def __repr__(self) -> str: def __str__(self) -> str: return self.key - - -class RoleLike(TypedDict, total = False): - """Base type for any data castable to a :class:`.Role`. - - .. versionadded:: 35.7.0 - - """ - - key: str - plural: Optional[str] - label: Optional[str] - doc: Optional[str] - max: Optional[int] - subroles: Optional[Sequence[str]] diff --git a/openfisca_core/types/_protocols.py b/openfisca_core/types/_protocols.py deleted file mode 100644 index bdd061e9fd..0000000000 --- a/openfisca_core/types/_protocols.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations - -import abc -from typing import Any, Optional, Sequence - -from typing_extensions import Protocol, runtime_checkable - - -class HasRoles(Protocol): - """Duck-type for entities. - - .. versionadded:: 35.7.0 - - """ - - roles: Sequence[SupportsRole] - - -class HasVariables(Protocol): - """Duck-type for tax-benefit systems. - - .. versionadded:: 35.7.0 - - """ - - @abc.abstractmethod - def get_variable(self, __arg1: str, __arg2: bool = False) -> Optional[Any]: - """A tax-benefit system implements :meth:`.get_variable`.""" - - -@runtime_checkable -class SupportsRole(Protocol): - """Duck-type for roles. - - .. versionadded:: 35.7.0 - - """ - - max: Optional[int] - subroles: Optional[Sequence[SupportsRole]] diff --git a/openfisca_core/types/__init__.py b/openfisca_core/typing/__init__.py similarity index 61% rename from openfisca_core/types/__init__.py rename to openfisca_core/typing/__init__.py index 04c07584d7..b21a285408 100644 --- a/openfisca_core/types/__init__.py +++ b/openfisca_core/typing/__init__.py @@ -7,15 +7,15 @@ Official Public API: * :data:`.ArrayLike` * :attr:`.ArrayType` - * :class:`.HasRoles` - * :class:`.HasVariables` - * :class:`.SupportsRole` + * :class:`.GroupEntityProtocol` + * :class:`.RoleProtocol` + * :class:`.TaxBenefitSystemProtocol` Note: How imports are being used today:: - from openfisca_core.types import * # Bad - from openfisca_core.types.data_types.arrays import ArrayLike # Bad + from openfisca_core.typing import * # Bad + from openfisca_core.typing._types import ArrayLike # Bad The previous examples provoke cyclic dependency problems, that prevents us @@ -24,7 +24,7 @@ How could them be used after the next major release:: - from openfisca_core.types import ArrayLike + from openfisca_core.typing import ArrayLike ArrayLike # Good: import types as publicly exposed @@ -40,7 +40,7 @@ # Official Public API -from ._data_types import ( # noqa: F401 +from ._types import ( # noqa: F401 ArrayLike, ArrayType, ) @@ -48,9 +48,16 @@ __all__ = ["ArrayLike", "ArrayType"] from ._protocols import ( # noqa: F401 - HasRoles, - HasVariables, - SupportsRole, + EntityProtocol, + FormulaProtocol, + GroupEntityProtocol, + RoleProtocol, + TaxBenefitSystemProtocol, ) -__all__ = ["HasRoles", "HasVariables", "SupportsRole", *__all__] +__all__ = ["EntityProtocol", "GroupEntityProtocol", "RoleProtocol", *__all__] +__all__ = ["FormulaProtocol", "TaxBenefitSystemProtocol", *__all__] + +from ._schemas import RoleSchema # noqa: F401 + +__all__ = ["RoleSchema", *__all__] diff --git a/openfisca_core/typing/_protocols.py b/openfisca_core/typing/_protocols.py new file mode 100644 index 0000000000..7d48955289 --- /dev/null +++ b/openfisca_core/typing/_protocols.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import Any, Optional, Sequence +from typing_extensions import Protocol, runtime_checkable + +import abc + + +class EntityProtocol(Protocol): + """Duck-type for entities. + + .. versionadded:: 35.8.0 + + """ + + key: str + plural: str + + +class FormulaProtocol(Protocol): + """Duck-type for formulas. + + .. versionadded:: 35.8.0 + + """ + + +class GroupEntityProtocol(EntityProtocol, Protocol): + """Duck-type for group entities. + + .. versionadded:: 35.8.0 + + """ + + roles: Sequence[RoleProtocol] + roles_description: Sequence[Any] + flattened_roles: Sequence[RoleProtocol] + + +@runtime_checkable +class RoleProtocol(Protocol): + """Duck-type for roles. + + .. versionadded:: 35.8.0 + + """ + + key: str + max: Optional[int] + subroles: Optional[Sequence[RoleProtocol]] + + +class TaxBenefitSystemProtocol(Protocol): + """Duck-type for tax-benefit systems. + + .. versionadded:: 35.8.0 + + """ + + @abc.abstractmethod + def get_variable(self, __arg1: str, __arg2: bool = False) -> Optional[Any]: + ... diff --git a/openfisca_core/typing/_schemas.py b/openfisca_core/typing/_schemas.py new file mode 100644 index 0000000000..186e2ef083 --- /dev/null +++ b/openfisca_core/typing/_schemas.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import Optional, Sequence +from typing_extensions import TypedDict + + +class RoleSchema(TypedDict, total = False): + """Data-schema for :class:`.Role`. + + .. versionadded:: 35.8.0 + + """ + + key: str + plural: Optional[str] + label: Optional[str] + doc: Optional[str] + max: Optional[int] + subroles: Optional[Sequence[str]] diff --git a/openfisca_core/types/_data_types.py b/openfisca_core/typing/_types.py similarity index 90% rename from openfisca_core/types/_data_types.py rename to openfisca_core/typing/_types.py index 07b4a036f8..943bfbe568 100644 --- a/openfisca_core/types/_data_types.py +++ b/openfisca_core/typing/_types.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Sequence, TypeVar, Union from nptyping import types, NDArray as ArrayType @@ -34,8 +36,11 @@ Todo: * Refactor once numpy version >= 1.21 is used. +.. versionchanged:: 35.8.0 + Moved to :mod:`.openfisca_core.typing` + .. versionchanged:: 35.6.0 - Moved to :mod:`.types` + Moved to ``openfisca_core.types`` .. versionadded:: 35.5.0 diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 4ab78156b2..191d48305f 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -18,7 +18,7 @@ check-style: $(shell git ls-files "*.py") lint-doc: \ lint-doc-commons \ lint-doc-entities \ - lint-doc-types \ + lint-doc-typing \ ; ## Run linters to check for syntax and style errors in the doc. @@ -44,7 +44,7 @@ check-types: lint-typing-strict: \ lint-typing-strict-commons \ lint-typing-strict-entities \ - lint-typing-strict-types \ + lint-typing-strict-typing \ ; ## Run static type checkers for type errors (strict). diff --git a/setup.cfg b/setup.cfg index cdecaae600..e55a9b51ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ extend-ignore = D hang-closing = true ignore = E128,E251,F403,F405,E501,RST301,W503,W504 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/types +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/typing rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, data, exc, func, meth, obj strictness = short @@ -41,7 +41,7 @@ skip_empty = true addopts = --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py -testpaths = openfisca_core/commons openfisca_core/entities openfisca_core/types tests +testpaths = openfisca_core/commons openfisca_core/entities openfisca_core/typing tests [mypy] ignore_missing_imports = True diff --git a/setup.py b/setup.py index 36e30a751e..6b4ef20dcf 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ setup( name = 'OpenFisca-Core', - version = '35.7.1', + version = '35.8.0', author = 'OpenFisca Team', author_email = 'contact@openfisca.org', classifiers = [ From 4604be52c8c53939f980ea4d684849988492e080 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 16:16:55 +0200 Subject: [PATCH 18/39] Extract role building to a function --- openfisca_core/entities/entity.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index ea233bb130..74893b20ba 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -59,7 +59,6 @@ class Entity: "plural", "label", "doc", - "is_person", "_tax_benefit_system", )) @@ -67,10 +66,13 @@ class Entity: plural: str label: str doc: str + is_person: bool = True - def __post_init__(self, *_args: Any) -> None: - self.doc = textwrap.dedent(self.doc) - self.is_person = True + def __init__(self, key: str, plural: str, label: str, doc: str) -> None: + self.key = key + self.label = label + self.plural = plural + self.doc = textwrap.dedent(doc) def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key})" From a62c4c9e5d33e8eecf8154b3d43208df87afecdb Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 16:36:44 +0200 Subject: [PATCH 19/39] Add pure flatten function to commons --- openfisca_core/typing/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openfisca_core/typing/__init__.py b/openfisca_core/typing/__init__.py index b21a285408..1194b97840 100644 --- a/openfisca_core/typing/__init__.py +++ b/openfisca_core/typing/__init__.py @@ -7,9 +7,15 @@ Official Public API: * :data:`.ArrayLike` * :attr:`.ArrayType` + * :class:`.EntityProtocol` + * :class:`.FormulaProtocol` * :class:`.GroupEntityProtocol` * :class:`.RoleProtocol` * :class:`.TaxBenefitSystemProtocol` + * :class:`.GroupEntityProtocol` + * :class:`.RoleProtocol` + * :class:`.TaxBenefitSystemProtocol` + * :class:`.RoleSchema` Note: How imports are being used today:: From e698fe4a71fd122ce4b8b81c9748a2c3cec48c22 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 16:58:18 +0200 Subject: [PATCH 20/39] Add slots to GroupEntity --- openfisca_core/entities/entity.py | 4 +++- openfisca_core/entities/group_entity.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 74893b20ba..99b90c2239 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -59,6 +59,7 @@ class Entity: "plural", "label", "doc", + "is_person", "_tax_benefit_system", )) @@ -66,13 +67,14 @@ class Entity: plural: str label: str doc: str - is_person: bool = True + is_person: bool def __init__(self, key: str, plural: str, label: str, doc: str) -> None: self.key = key self.label = label self.plural = plural self.doc = textwrap.dedent(doc) + self.is_person = True def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key})" diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index ee0ac48247..807a7263f6 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -71,7 +71,6 @@ class GroupEntity(Entity): ... "subroles": ["first_partner", "second_partner"], ... }] - >>> household = GroupEntity( ... "household", ... "households", From 92312c561fed4c03160d624d40f4bb00b6c6f893 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 17:47:43 +0200 Subject: [PATCH 21/39] Fix mutability of group entity --- openfisca_core/entities/group_entity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index 807a7263f6..cc5c2515bb 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 Any, Dict, Iterable, Sequence +from typing import Any, Iterable, Mapping, Sequence from openfisca_core.typing import RoleProtocol, RoleSchema import dataclasses @@ -117,7 +117,7 @@ class GroupEntity(Entity): containing_entities: Sequence[str] flattened_roles: Sequence[RoleProtocol] roles_description: Sequence[RoleSchema] - _cached_roles: Dict[str, RoleProtocol] + _cached_roles: Mapping[str, RoleProtocol] def __init__( self, @@ -226,7 +226,7 @@ def _flatten_roles(roles: Sequence[RoleProtocol]) -> Sequence[RoleProtocol]: return tuple(chain.from_iterable(tree)) -def _cache_roles(roles: Sequence[RoleProtocol]) -> Dict[str, RoleProtocol]: +def _cache_roles(roles: Sequence[RoleProtocol]) -> Mapping[str, RoleProtocol]: """Create a cached dictionary of :obj:`.Role`. Args: From be3753dfabb0e5cf369df7a07dd234c48bd7c4ea Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 24 Oct 2021 20:10:12 +0200 Subject: [PATCH 22/39] Move role building out of GroupEntity --- openfisca_core/entities/__init__.py | 24 +++++++++++++++++++----- openfisca_core/entities/helpers.py | 9 +++++---- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index a6aa21268d..74ac93310c 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -41,7 +41,9 @@ * :class:`.GroupEntity` * :class:`.Role` * :func:`.build_entity` + * :func:`.build_role` * :func:`.check_role_validity` + * :func:`.flatten_roles` Deprecated: * :meth:`.Entity.set_tax_benefit_system` @@ -83,10 +85,22 @@ # Official Public API -from .role import Role # noqa: F401 from .entity import Entity # noqa: F401 from .group_entity import GroupEntity # noqa: F401 -from .helpers import build_entity, check_role_validity # noqa: F401 - -__all__ = ["Entity", "GroupEntity", "Role"] -__all__ = ["build_entity", "check_role_validity", *__all__] +from .role import Role # noqa: F401 +from .helpers import ( # noqa: F401 + build_entity, + build_role, + check_role_validity, + flatten_roles, + ) + +__all__ = [ + "Entity", + "GroupEntity", + "Role", + "build_entity", + "build_role", + "check_role_validity", + "flatten_roles", + ] diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 0d9c93e024..03f3132c60 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import Any, Optional, Sequence, Union -from openfisca_core.typing import RoleProtocol, RoleSchema +from typing import Any, Optional, Sequence +from openfisca_core.typing import EntityProtocol, RoleSchema from .entity import Entity from .group_entity import GroupEntity +from .role import Role def build_entity( @@ -16,7 +17,7 @@ def build_entity( is_person: bool = False, class_override: Optional[Any] = None, containing_entities: Sequence[str] = (), - ) -> Union[Entity, GroupEntity]: + ) -> EntityProtocol: """Builds an :class:`.Entity` or a :class:`.GroupEntity`. Args: @@ -102,5 +103,5 @@ def check_role_validity(role: Any) -> None: """ - if role is not None and not isinstance(role, RoleProtocol): + if role is not None and not isinstance(role, Role): raise ValueError(f"{role} is not a valid role") From 5c534ad8b1c13b5bc638ac293f2822a6bc1f395b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 25 Oct 2021 09:45:11 +0200 Subject: [PATCH 23/39] Add the variables descriptor --- openfisca_core/entities/_variable_proxy.py | 171 +++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 openfisca_core/entities/_variable_proxy.py diff --git a/openfisca_core/entities/_variable_proxy.py b/openfisca_core/entities/_variable_proxy.py new file mode 100644 index 0000000000..5f1128416d --- /dev/null +++ b/openfisca_core/entities/_variable_proxy.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +from typing import Any, Optional, Type +from typing_extensions import Protocol + +import functools +import os + +from openfisca_core.types import Representable, HasVariables, SupportsFormula + +doc_url = "https://openfisca.org/doc/coding-the-legislation" + + +class _Query(Protocol): + """A dummy class to duck-type :meth:`.TaxBenefitSystem.get_variable`.""" + + def __call__( + self, + __arg1: str, + __arg2: bool = False, + ) -> Optional["_VariableProxy"]: + """See comment above.""" + + +class _VariableProxy: + """A `descriptor`_ to find an :obj:`.Entity`'s :obj:`.Variable`. + + Attributes: + entity: The :obj:`.Entity` ``owner`` of the descriptor. + tax_benefit_system: The :obj:`.Entity`'s :obj:`.TaxBenefitSystem`. + query: The method used to query the :obj:`.TaxBenefitSystem`. + + Examples: + >>> from openfisca_core.entities import Entity + >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + >>> from openfisca_core.variables import Variable + + >>> entity = Entity( + ... "individual", + ... "individuals", + ... "An individual", + ... "The minimal legal entity on which a rule can be applied.", + ... ) + + >>> class Variable(Variable): + ... definition_period = "month" + ... value_type = float + ... entity = entity + + >>> tbs = TaxBenefitSystem([entity]) + >>> tbs.add_variable(Variable) + >> entity.tax_benefit_system = tbs + + >>> entity.variables.get("Variable") + <...Variable... + + >>> entity.variables.exists().get("Variable") + <...Variable... + + >>> entity.variables.isdefined().get("Variable") + <...Variable... + + .. _descriptor: https://docs.python.org/3/howto/descriptor.html + + .. versionadded:: 35.7.0 + + """ + + entity: Optional[Representable] = None + tax_benefit_system: Optional[HasVariables] = None + query: _Query + + def __get__( + self, + entity: Representable, + type: Type[Representable], + ) -> Optional[_VariableProxy]: + """Binds :meth:`.TaxBenefitSystem.get_variable`.""" + + self.entity = entity + + self.tax_benefit_system = getattr( + self.entity, + "tax_benefit_system", + None, + ) + + if self.tax_benefit_system is None: + return None + + self.query = self.tax_benefit_system.get_variable + + return self + + def __set__(self, entity: Representable, value: Any) -> None: + NotImplemented + + def get(self, variable_name: str) -> Optional[SupportsFormula]: + """Runs the query for ``variable_name``, based on the options given. + + Args: + variable_name: The :obj:`.Variable` to be found. + + Returns: + :obj:`.Variable` or :obj:`None`: + :obj:`.Variable` when the :obj:`.Variable` exists. + :obj:`None` when the :attr:`.tax_benefit_system` is not set. + + Raises: + :exc:`.VariableNotFoundError`: When :obj:`.Variable` doesn't exist. + :exc:`.ValueError`: When the :obj:`.Variable` exists but is defined + for another :obj:`.Entity`. + + .. versionadded:: 35.7.0 + + """ + + if self.entity is None: + return NotImplemented + + return self.query(variable_name) + + def exists(self) -> _VariableProxy: + """Sets ``check_existence`` to ``True``.""" + + self.query = functools.partial( + self.query, + check_existence = True, + ) + + return self + + def isdefined(self) -> _VariableProxy: + """Checks that ``variable_name`` is defined for :attr:`.entity`.""" + + # We assume that we're also checking for existance. + self.exists() + + self.query = functools.partial( + self._isdefined, + self.query, + ) + + return self + + def _isdefined(self, query: _Query, variable_name: str, **any: Any) -> Any: + variable = query(variable_name) + + if self.entity is None: + return None + + if variable is None: + return None + + if variable.entity is None: + return None + + if self.entity != variable.entity: + message = os.linesep.join([ + f"You tried to compute the variable '{variable_name}' for", + f"the entity '{self.entity.plural}'; however the variable", + f"'{variable_name}' is defined for the entity", + f"'{variable.entity.plural}'. Learn more about entities", + f"in our documentation: <{doc_url}/50_entities.html>.", + ]) + + raise ValueError(message) + + return variable From 6acd4a90b903811c44ea86f8abc1da68c1cb0c98 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 25 Oct 2021 09:52:17 +0200 Subject: [PATCH 24/39] Add descriptor to Entity --- openfisca_core/entities/__init__.py | 21 +-- openfisca_core/entities/_variable_proxy.py | 23 +-- openfisca_core/entities/entity.py | 179 ++++++++++++++++++--- openfisca_core/entities/group_entity.py | 4 +- openfisca_core/typing/__init__.py | 3 + openfisca_core/typing/_protocols.py | 10 ++ setup.cfg | 1 + 7 files changed, 187 insertions(+), 54 deletions(-) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index 74ac93310c..3c43da7e8f 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -41,9 +41,7 @@ * :class:`.GroupEntity` * :class:`.Role` * :func:`.build_entity` - * :func:`.build_role` * :func:`.check_role_validity` - * :func:`.flatten_roles` Deprecated: * :meth:`.Entity.set_tax_benefit_system` @@ -85,22 +83,13 @@ # Official Public API -from .entity import Entity # noqa: F401 -from .group_entity import GroupEntity # noqa: F401 -from .role import Role # noqa: F401 from .helpers import ( # noqa: F401 + Entity, + GroupEntity, + Role, build_entity, - build_role, check_role_validity, - flatten_roles, ) -__all__ = [ - "Entity", - "GroupEntity", - "Role", - "build_entity", - "build_role", - "check_role_validity", - "flatten_roles", - ] +__all__ = ["Entity", "GroupEntity", "Role"] +__all__ = ["build_entity", "check_role_validity", *__all__] diff --git a/openfisca_core/entities/_variable_proxy.py b/openfisca_core/entities/_variable_proxy.py index 5f1128416d..cbae01007f 100644 --- a/openfisca_core/entities/_variable_proxy.py +++ b/openfisca_core/entities/_variable_proxy.py @@ -2,12 +2,15 @@ from typing import Any, Optional, Type from typing_extensions import Protocol +from openfisca_core.typing import ( + EntityProtocol, + TaxBenefitSystemProtocol, + VariableProtocol, + ) import functools import os -from openfisca_core.types import Representable, HasVariables, SupportsFormula - doc_url = "https://openfisca.org/doc/coding-the-legislation" @@ -64,18 +67,18 @@ class _VariableProxy: .. _descriptor: https://docs.python.org/3/howto/descriptor.html - .. versionadded:: 35.7.0 + .. versionadded:: 35.8.0 """ - entity: Optional[Representable] = None - tax_benefit_system: Optional[HasVariables] = None + entity: Optional[EntityProtocol] = None + tax_benefit_system: Optional[TaxBenefitSystemProtocol] = None query: _Query def __get__( self, - entity: Representable, - type: Type[Representable], + entity: EntityProtocol, + type: Type[EntityProtocol], ) -> Optional[_VariableProxy]: """Binds :meth:`.TaxBenefitSystem.get_variable`.""" @@ -94,10 +97,10 @@ def __get__( return self - def __set__(self, entity: Representable, value: Any) -> None: + def __set__(self, entity: EntityProtocol, value: Any) -> None: NotImplemented - def get(self, variable_name: str) -> Optional[SupportsFormula]: + def get(self, variable_name: str) -> Optional[VariableProtocol]: """Runs the query for ``variable_name``, based on the options given. Args: @@ -113,7 +116,7 @@ def get(self, variable_name: str) -> Optional[SupportsFormula]: :exc:`.ValueError`: When the :obj:`.Variable` exists but is defined for another :obj:`.Entity`. - .. versionadded:: 35.7.0 + .. versionadded:: 35.8.0 """ diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 99b90c2239..b2e3f62ec4 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,17 +1,19 @@ from __future__ import annotations from typing import Any, Optional -from openfisca_core.typing import TaxBenefitSystemProtocol +from openfisca_core.typing import ( + RoleProtocol, + TaxBenefitSystemProtocol, + VariableProtocol, + ) -import os import textwrap -from dataclasses import dataclass +import dataclasses +from ._variable_proxy import _VariableProxy -from .. import entities - -@dataclass +@dataclasses.dataclass class Entity: """Represents an entity on which calculations can be run. @@ -37,7 +39,7 @@ class Entity: ... "individual", ... "individuals", ... "An individual", - ... "The minimal legal entity on which a rule might be applied.", + ... "\t\t\tThe minimal legal entity on which a rule might be a...", ... ) >>> repr(Entity) @@ -68,6 +70,11 @@ class Entity: label: str doc: str is_person: bool + _variables: _VariableProxy = dataclasses.field( + init = False, + compare = False, + default = _VariableProxy(), + ) def __init__(self, key: str, plural: str, label: str, doc: str) -> None: self.key = key @@ -92,7 +99,10 @@ def tax_benefit_system(self) -> Optional[TaxBenefitSystemProtocol]: def tax_benefit_system(self, value: TaxBenefitSystemProtocol) -> None: self._tax_benefit_system = value - def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystemProtocol) -> None: + def set_tax_benefit_system( + self, + tax_benefit_system: TaxBenefitSystemProtocol, + ) -> None: """Sets ``_tax_benefit_system``. Args: @@ -107,6 +117,12 @@ def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystemProtocol) - self.tax_benefit_system = tax_benefit_system + @property + def variables(self) -> Optional[_VariableProxy]: + """An :class:`.Entity` has many :class:`Variables <.Variable>`.""" + + return self._variables + @staticmethod def check_role_validity(role: Any) -> None: """Checks if ``role`` is an instance of :class:`.Role`. @@ -114,8 +130,8 @@ def check_role_validity(role: Any) -> None: Args: role: Any object. - Returns: - None. + Raises: + ValueError: When ``role`` is not a :class:`.Role`. .. deprecated:: 35.8.0 :meth:`.check_role_validity` has been deprecated and will be @@ -124,19 +140,130 @@ def check_role_validity(role: Any) -> None: """ - return entities.check_role_validity(role) - - def get_variable(self, variable_name, check_existence = False): - return self._tax_benefit_system.get_variable(variable_name, check_existence) - - def check_variable_defined_for_entity(self, variable_name): - variable_entity = self.get_variable(variable_name, check_existence = True).entity - # Should be this: - # if variable_entity is not self: - if variable_entity.key != self.key: - message = os.linesep.join([ - "You tried to compute the variable '{0}' for the entity '{1}';".format(variable_name, self.plural), - "however the variable '{0}' is defined for '{1}'.".format(variable_name, variable_entity.plural), - "Learn more about entities in our documentation:", - "."]) - raise ValueError(message) + if role is not None and not isinstance(role, RoleProtocol): + raise ValueError(f"{role} is not a valid role") + + def get_variable( + self, + variable_name: str, + check_existence: bool = False, + ) -> Optional[VariableProtocol]: + """Gets ``variable_name`` from ``variables``. + + Args: + variable_name: The variable to be found. + check_existence: Was the variable found? Defaults to False. + + Returns: + :obj:`.Variable` or :obj:`None`: + :obj:`.Variable` when the :obj:`.Variable` exists. + :obj:`None` when the :obj:`.Variable` doesn't exist. + + Raises: + :exc:`.VariableNotFoundError`: When ``check_existence`` is True and + the :obj:`.Variable` doesn't exist. + + Examples: + >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + >>> from openfisca_core.variables import Variable + + >>> this = Entity("this", "", "", "") + >>> that = Entity("that", "", "", "") + + >>> this.get_variable("foo") + + >>> this.get_variable("foo", check_existence = True) + + >>> this.tax_benefit_system = TaxBenefitSystem([that]) + + >>> this.get_variable("foo") + + >>> this.get_variable("foo", check_existence = True) + Traceback (most recent call last): + VariableNotFoundError: You tried to calculate or to set a value... + + >>> class foo(Variable): + ... definition_period = "month" + ... value_type = float + ... entity = that + + >>> this.tax_benefit_system.add_variable(foo) + + + >>> this.get_variable("foo") + + + .. versionchanged:: 35.8.0 + Added documentation, doctests, and typing. + + """ + + if self.variables is None: + return None + + if check_existence: + return self.variables.exists().get(variable_name) + + return self.variables.get(variable_name) + + def check_variable_defined_for_entity( + self, + variable_name: str, + ) -> Optional[VariableProtocol]: + """Checks if ``variable_name`` is defined for ``self``. + + Args: + variable_name: The :obj:`.Variable` to be found. + + Returns: + :obj:`.Variable` or :obj:`None`: + :obj:`.Variable` when the :obj:`.Variable` exists. + :obj:`None` when the :attr:`.tax_benefit_system` is not set. + + Raises: + :exc:`.VariableNotFoundError`: When :obj:`.Variable` doesn't exist. + :exc:`.ValueError`: When the :obj:`.Variable` exists but is defined + for another :obj:`.Entity`. + + Examples: + >>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem + >>> from openfisca_core.variables import Variable + + >>> this = Entity("this", "", "", "") + >>> that = Entity("that", "", "", "") + + >>> this.check_variable_defined_for_entity("foo") + + >>> this.tax_benefit_system = TaxBenefitSystem([that]) + >>> this.check_variable_defined_for_entity("foo") + Traceback (most recent call last): + VariableNotFoundError: You tried to calculate or to set a value... + + >>> class foo(Variable): + ... definition_period = "month" + ... value_type = float + ... entity = that + + >>> this.tax_benefit_system.add_variable(foo) + + + >>> this.check_variable_defined_for_entity("foo") + Traceback (most recent call last): + ValueError: You tried to compute the variable 'foo' for the enti... + + >>> foo.entity = this + >>> this.tax_benefit_system.update_variable(foo) + + + >>> this.check_variable_defined_for_entity("foo") + + + .. versionchanged:: 35.8.0 + Added documentation, doctests, and typing. + + """ + + if self.variables is None: + return None + + return self.variables.isdefined().get(variable_name) diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index cc5c2515bb..22ff6e3bbd 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -62,7 +62,7 @@ class GroupEntity(Entity): ... "family", ... "families", ... "A family", - ... "All the people somehow related living together.", + ... "\t\t\tAll the people somehow related living together.", ... family_roles, ... ) @@ -75,7 +75,7 @@ class GroupEntity(Entity): ... "household", ... "households", ... "A household", - ... "All the people who live together in the same place.", + ... "\t\t\tAll the people who live together in the same place.", ... household_roles, ... (family.key,), ... ) diff --git a/openfisca_core/typing/__init__.py b/openfisca_core/typing/__init__.py index 1194b97840..cc20997c5c 100644 --- a/openfisca_core/typing/__init__.py +++ b/openfisca_core/typing/__init__.py @@ -15,6 +15,7 @@ * :class:`.GroupEntityProtocol` * :class:`.RoleProtocol` * :class:`.TaxBenefitSystemProtocol` + * :class:`.VariableProtocol` * :class:`.RoleSchema` Note: @@ -59,10 +60,12 @@ GroupEntityProtocol, RoleProtocol, TaxBenefitSystemProtocol, + VariableProtocol, ) __all__ = ["EntityProtocol", "GroupEntityProtocol", "RoleProtocol", *__all__] __all__ = ["FormulaProtocol", "TaxBenefitSystemProtocol", *__all__] +__all__ = ["VariableProtocol", *__all__] from ._schemas import RoleSchema # noqa: F401 diff --git a/openfisca_core/typing/_protocols.py b/openfisca_core/typing/_protocols.py index 7d48955289..b1af51ef44 100644 --- a/openfisca_core/typing/_protocols.py +++ b/openfisca_core/typing/_protocols.py @@ -1,3 +1,5 @@ +# pylint: disable=missing-function-docstring + from __future__ import annotations from typing import Any, Optional, Sequence @@ -60,3 +62,11 @@ class TaxBenefitSystemProtocol(Protocol): @abc.abstractmethod def get_variable(self, __arg1: str, __arg2: bool = False) -> Optional[Any]: ... + + +class VariableProtocol(Protocol): + """Duck-type for variables. + + .. versionadded:: 35.8.0 + + """ diff --git a/setup.cfg b/setup.cfg index e55a9b51ed..1da9273251 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,6 +15,7 @@ hang-closing = true ignore = E128,E251,F403,F405,E501,RST301,W503,W504 in-place = true include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/typing +per-file-ignores = openfisca_core/typing/_protocols.py:D102 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, data, exc, func, meth, obj strictness = short From 14ff209a71fd87122a832a07b4e30baa7e1304f2 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 25 Oct 2021 09:56:00 +0200 Subject: [PATCH 25/39] Add docs to Entity.get_variable --- openfisca_core/entities/entity.py | 10 +++++----- openfisca_core/entities/role.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index b2e3f62ec4..0732f7a846 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -7,8 +7,8 @@ VariableProtocol, ) -import textwrap import dataclasses +import textwrap from ._variable_proxy import _VariableProxy @@ -57,12 +57,12 @@ class Entity: """ __slots__ = tuple(( - "key", - "plural", - "label", + "_tax_benefit_system", "doc", "is_person", - "_tax_benefit_system", + "key", + "label", + "plural", )) key: str diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index a3c28b5e7a..53425cc180 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -34,7 +34,7 @@ class Role: ... "key": "parent", ... "label": "Parents", ... "plural": "parents", - ... "doc": "The one or two adults in charge of the household.", + ... "doc": "\t\t\tThe one/two adults in charge of the household.", ... "max": 2, ... } From fa6de71dbe567ce871071632fdfa99981c81186f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 25 Oct 2021 10:22:26 +0200 Subject: [PATCH 26/39] Add docs to entities.build_role --- openfisca_core/entities/helpers.py | 5 ++++- openfisca_core/entities/role.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 03f3132c60..857e109edf 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -95,10 +95,13 @@ def check_role_validity(role: Any) -> None: ValueError: When ``role`` is not a :class:`.Role`. Examples: - >>> from openfisca_core.entities import Role >>> role = Role({"key": "key"}, object()) >>> check_role_validity(role) + >>> check_role_validity("hey!") + Traceback (most recent call last): + ValueError: hey! is not a valid role + .. versionadded:: 35.8.0 """ diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 53425cc180..5d282851df 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -4,10 +4,10 @@ from openfisca_core.typing import GroupEntityProtocol, RoleProtocol, RoleSchema import textwrap -from dataclasses import dataclass +import dataclasses -@dataclass +@dataclasses.dataclass class Role: """Role of an :class:`.Entity` within a :class:`.GroupEntity`. From eaa7f37b088a828ccebb65c345280e49a6f46d03 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 25 Oct 2021 11:03:35 +0200 Subject: [PATCH 27/39] Add exceptions to doctests --- 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 857e109edf..9825795581 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -36,7 +36,7 @@ def build_entity( :obj:`.GroupEntity`: When ``is_person`` is False. Raises: - ValueError: If ``roles`` is not iterable. + ValueError: If ``roles`` is not a sequence. Examples: >>> build_entity( From 02370cceb3a6ef30ab1e9d6ada86f56f4a02d9e7 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 25 Oct 2021 11:43:54 +0200 Subject: [PATCH 28/39] Cleanup protocol usage in entities --- openfisca_core/entities/role.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 5d282851df..65ffd81138 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -69,6 +69,7 @@ def __init__( description: RoleSchema, entity: GroupEntityProtocol, ) -> None: + self.entity = entity self.key = description['key'] self.plural = description.get('plural') From e1f06fdaa2580316350476d87846e4f4b723c01c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 25 Oct 2021 12:08:15 +0200 Subject: [PATCH 29/39] Fix cyclic imports 2/2 --- openfisca_core/entities/group_entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index 22ff6e3bbd..660c1b90fa 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -130,6 +130,7 @@ def __init__( ): super().__init__(key, plural, label, doc) + self.is_person = False self.containing_entities = containing_entities self.roles = tuple(_build_role(self, desc) for desc in roles) self.flattened_roles = _flatten_roles(self.roles) From edd3d1a67d9b259644f495147f21e4b6873c1c3a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 25 Oct 2021 12:45:02 +0200 Subject: [PATCH 30/39] Rename RoleLike to _RoleSchema --- 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 65ffd81138..f4adee63c2 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -3,8 +3,8 @@ from typing import Optional, Sequence from openfisca_core.typing import GroupEntityProtocol, RoleProtocol, RoleSchema -import textwrap import dataclasses +import textwrap @dataclasses.dataclass From 3f70799b6620d44eb64a3ce21b7e9e18afcaf47f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Oct 2021 11:36:26 +0200 Subject: [PATCH 31/39] Add missing tests --- openfisca_core/entities/tests/__init__.py | 0 openfisca_core/entities/tests/test_entity.py | 11 ++ .../entities/tests/test_group_entity.py | 39 +++++ openfisca_core/entities/tests/test_role.py | 11 ++ .../entities/tests/test_variable_proxy.py | 154 ++++++++++++++++++ setup.cfg | 3 + 6 files changed, 218 insertions(+) create mode 100644 openfisca_core/entities/tests/__init__.py create mode 100644 openfisca_core/entities/tests/test_entity.py create mode 100644 openfisca_core/entities/tests/test_group_entity.py create mode 100644 openfisca_core/entities/tests/test_role.py create mode 100644 openfisca_core/entities/tests/test_variable_proxy.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_entity.py b/openfisca_core/entities/tests/test_entity.py new file mode 100644 index 0000000000..a4ec2f686d --- /dev/null +++ b/openfisca_core/entities/tests/test_entity.py @@ -0,0 +1,11 @@ +from openfisca_core.entities import Entity + + +def test_init_when_doc_indented(): + """Dedents the ``doc`` attribute if it is passed at initialisation.""" + + key = "\tkey" + doc = "\tdoc" + entity = Entity(key, "label", "plural", doc) + assert entity.key == key + assert entity.doc != doc 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..7362bf3eb6 --- /dev/null +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -0,0 +1,39 @@ +import pytest + +from openfisca_core.entities import GroupEntity + + +@pytest.fixture +def roles(): + """A role-like.""" + + return [{"key": "parent", "subroles": ["first_parent", "second_parent"]}] + + +@pytest.fixture +def group_entity(roles): + """A group entity.""" + + return GroupEntity("key", "label", "plural", "doc", roles) + + +def test_init_when_doc_indented(): + """Dedents the ``doc`` attribute if it is passed at initialisation.""" + + key = "\tkey" + doc = "\tdoc" + group_entity = GroupEntity(key, "label", "plural", doc, []) + assert group_entity.key == key + assert group_entity.doc != doc + + +def test_group_entity_with_roles(group_entity): + """Assigns a :obj:`.Role` for each role-like passed as argument.""" + + assert group_entity.PARENT + + +def test_group_entity_with_subroles(group_entity): + """Assigns a :obj:`.Role` for each subrole-like passed as argument.""" + + assert group_entity.FIRST_PARENT diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py new file mode 100644 index 0000000000..0953a8689c --- /dev/null +++ b/openfisca_core/entities/tests/test_role.py @@ -0,0 +1,11 @@ +from openfisca_core.entities import Role + + +def test_init_when_doc_indented(): + """Dedents the ``doc`` attribute if it is passed at initialisation.""" + + key = "\tkey" + doc = "\tdoc" + role = Role({"key": key, "doc": doc}, object()) + assert role.key == key + assert role.doc != doc diff --git a/openfisca_core/entities/tests/test_variable_proxy.py b/openfisca_core/entities/tests/test_variable_proxy.py new file mode 100644 index 0000000000..8708d3b9a7 --- /dev/null +++ b/openfisca_core/entities/tests/test_variable_proxy.py @@ -0,0 +1,154 @@ +import pytest + +from openfisca_core.entities import Entity +from openfisca_core.errors import VariableNotFoundError +from openfisca_core.taxbenefitsystems import TaxBenefitSystem +from openfisca_core.variables import Variable + +from .._variable_proxy import _VariableProxy + + +@pytest.fixture() +def variables(): + """A variable descriptor.""" + + return _VariableProxy() + + +@pytest.fixture +def entity(): + """An entity.""" + + return Entity("", "individuals", "", "") + + +@pytest.fixture +def ThisVar(entity): + """A variable.""" + + return type( + "ThisVar", + (Variable,), + { + "definition_period": "month", + "value_type": float, + "entity": entity, + }, + ) + + +@pytest.fixture +def ThatVar(): + """Another variable.""" + + class ThatVar(Variable): + """Another variable.""" + + definition_period = "month" + value_type = float + entity = Entity("", "martians", "", "") + + return ThatVar + + +@pytest.fixture +def tbs(entity, ThisVar, ThatVar): + """A tax-benefit system.""" + + tbs = TaxBenefitSystem([entity]) + tbs.add_variable(ThisVar) + tbs.add_variable(ThatVar) + return tbs + + +def test_variables_without_variable_name(variables): + """Raises a TypeError when called without ``variable_name``.""" + + with pytest.raises(TypeError, match = "'variable_name'"): + variables.get() + + +def test_variables_without_owner(variables): + """Returns NotImplemented when called without an ``owner``.""" + + assert variables.get("ThisVar") == NotImplemented + + +def test_variables_without_tax_benefit_system(entity): + """Returns None when called without a TaxBenefitSystem.""" + + assert not entity.variables + + +def test_variables_when_exists(entity, tbs, ThisVar): + """Returns the variable when it exists.""" + + entity.tax_benefit_system = tbs + variable = entity.variables.get("ThisVar") + assert isinstance(variable, ThisVar) + + +def test_variables_when_doesnt_exist(entity, tbs): + """Returns None when it does not.""" + + entity.tax_benefit_system = tbs + assert not entity.variables.get("OtherVar") + + +def test_variables_when_exists_and_check_exists(entity, tbs, ThisVar): + """Raises a VariableNotFoundError when checking for existance.""" + + entity.tax_benefit_system = tbs + variable = entity.variables.exists().get("ThisVar") + assert isinstance(variable, ThisVar) + + +def test_variables_when_doesnt_exist_and_check_exists(entity, tbs): + """Raises a VariableNotFoundError when checking for existance.""" + + entity.tax_benefit_system = tbs + + with pytest.raises(VariableNotFoundError, match = "'OtherVar'"): + entity.variables.exists().get("OtherVar") + + +def test_variables_when_exists_and_defined_for(entity, tbs, ThisVar): + """Returns the variable when it exists and defined for the entity.""" + + entity.tax_benefit_system = tbs + variable = entity.variables.isdefined().get("ThisVar") + assert isinstance(variable, ThisVar) + + +def test_variables_when_exists_and_not_defined_for(entity, tbs): + """Raises a ValueError when it exists but defined for another var.""" + + entity.tax_benefit_system = tbs + + with pytest.raises(ValueError, match = "'martians'"): + entity.variables.isdefined().get("ThatVar") + + +def test_variables_when_doesnt_exist_and_check_defined_for(entity, tbs): + """Raises a VariableNotFoundError when it doesn't exist.""" + + entity.tax_benefit_system = tbs + + with pytest.raises(VariableNotFoundError, match = "'OtherVar'"): + entity.variables.isdefined().get("OtherVar") + + +def test_variables_composition(entity, tbs): + """Conditions can be composed.""" + + entity.tax_benefit_system = tbs + + assert entity.variables.exists().isdefined().get("ThisVar") + + +def test_variables_permutation(entity, tbs): + """Conditions can be permuted.""" + + entity.tax_benefit_system = tbs + + assert entity.variables.isdefined().exists().get("ThisVar") diff --git a/setup.cfg b/setup.cfg index 1da9273251..add99164c5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,5 +52,8 @@ non_interactive = True [mypy-openfisca_core.commons.tests.*] ignore_errors = True +[mypy-openfisca_core.entities.tests.*] +ignore_errors = True + [mypy-openfisca_core.scripts.*] ignore_errors = True From 590fe5dffeced1818642f41e3f61eec275746e90 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Oct 2021 11:50:39 +0200 Subject: [PATCH 32/39] Fix generated doc --- openfisca_core/variables/variable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 61a5d9274f..acfeb9fe70 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -309,7 +309,7 @@ def get_formula(self, period = None): If no period is given and the variable has several formula, return the oldest formula. :returns: Formula used to compute the variable - :rtype: .Formula + :rtype: callable """ From 1d021148e8ed1d21ffb6295f02c9d5925445efa9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Oct 2021 12:56:44 +0200 Subject: [PATCH 33/39] Cache roles in GroupEntity --- openfisca_core/entities/tests/test_group_entity.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openfisca_core/entities/tests/test_group_entity.py b/openfisca_core/entities/tests/test_group_entity.py index 7362bf3eb6..aa7c03f8a5 100644 --- a/openfisca_core/entities/tests/test_group_entity.py +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -32,8 +32,14 @@ def test_group_entity_with_roles(group_entity): assert group_entity.PARENT + with pytest.raises(AttributeError): + assert group_entity.CHILD + def test_group_entity_with_subroles(group_entity): """Assigns a :obj:`.Role` for each subrole-like passed as argument.""" assert group_entity.FIRST_PARENT + + with pytest.raises(AttributeError): + assert group_entity.FIRST_CHILD From d04d0d14365cac3a127369bbf5c30fde93bd1b3e Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Oct 2021 12:03:05 +0200 Subject: [PATCH 34/39] Fix imports --- openfisca_core/entities/tests/test_variable_proxy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openfisca_core/entities/tests/test_variable_proxy.py b/openfisca_core/entities/tests/test_variable_proxy.py index 8708d3b9a7..e37f31aa21 100644 --- a/openfisca_core/entities/tests/test_variable_proxy.py +++ b/openfisca_core/entities/tests/test_variable_proxy.py @@ -1,12 +1,11 @@ import pytest from openfisca_core.entities import Entity +from openfisca_core.entities._variable_proxy import _VariableProxy from openfisca_core.errors import VariableNotFoundError from openfisca_core.taxbenefitsystems import TaxBenefitSystem from openfisca_core.variables import Variable -from .._variable_proxy import _VariableProxy - @pytest.fixture() def variables(): From f6b67d8aa102f9c37811bb9b764775a0f171ed21 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Oct 2021 13:29:30 +0200 Subject: [PATCH 35/39] Remove unused commons.first --- openfisca_core/commons/formulas.py | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 86636f1bc1..78ffc9adeb 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -91,6 +91,41 @@ def concat(this: ArrayLike[str], that: ArrayLike[str]) -> ArrayType[str]: return numpy.char.add(this, that) +<<<<<<< HEAD +======= +def flatten(seqs: ArrayLike[ArrayLike[T]]) -> Iterator[T]: + """Flattens a sequences of sequences. + + Args: + seqs: Any sequence of sequences. + + Returns: + An iterator with the values. + + Examples: + >>> list(flatten([(1, 2), (3, 4)])) + [1, 2, 3, 4] + + >>> list(flatten(["ab", "cd"])) + ['a', 'b', 'c', 'd'] + + >>> seqs = numpy.array([[1, 2, 3], [4, 5], [[6, [7]]]]) + + >>> list(flatten(seqs)) + [1, 2, 3, 4, 5, [6, [7]]] + + >>> list(flatten(flatten(seqs))) + Traceback (most recent call last): + TypeError: 'int' object is not iterable + + .. versionadded:: 35.7.0 + + """ + + return chain.from_iterable(seqs) + + +>>>>>>> 3230e5b1e (Remove unused commons.first) def switch( conditions: ArrayType[Any], value_by_condition: Dict[float, T], From 03860d9a62f7cea2277439860c0697798be55b04 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 26 Oct 2021 13:35:06 +0200 Subject: [PATCH 36/39] Remove underused commons.flatten --- openfisca_core/commons/formulas.py | 35 ------------------------------ 1 file changed, 35 deletions(-) diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 78ffc9adeb..86636f1bc1 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -91,41 +91,6 @@ def concat(this: ArrayLike[str], that: ArrayLike[str]) -> ArrayType[str]: return numpy.char.add(this, that) -<<<<<<< HEAD -======= -def flatten(seqs: ArrayLike[ArrayLike[T]]) -> Iterator[T]: - """Flattens a sequences of sequences. - - Args: - seqs: Any sequence of sequences. - - Returns: - An iterator with the values. - - Examples: - >>> list(flatten([(1, 2), (3, 4)])) - [1, 2, 3, 4] - - >>> list(flatten(["ab", "cd"])) - ['a', 'b', 'c', 'd'] - - >>> seqs = numpy.array([[1, 2, 3], [4, 5], [[6, [7]]]]) - - >>> list(flatten(seqs)) - [1, 2, 3, 4, 5, [6, [7]]] - - >>> list(flatten(flatten(seqs))) - Traceback (most recent call last): - TypeError: 'int' object is not iterable - - .. versionadded:: 35.7.0 - - """ - - return chain.from_iterable(seqs) - - ->>>>>>> 3230e5b1e (Remove unused commons.first) def switch( conditions: ArrayType[Any], value_by_condition: Dict[float, T], From 845235dd4c61a84b9605ac20c93e2a9475d211ba Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 25 Oct 2021 12:48:21 +0200 Subject: [PATCH 37/39] Bump minor to 35.8.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d95ddaa3..9844ee02b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 35.8.0 [#1034](https://github.com/openfisca/openfisca-core/pull/1034) + +#### Documentation + +- Complete docs, doctests, and typing of the entities module + ### 35.7.1 [#1075](https://github.com/openfisca/openfisca-core/pull/1075) #### Bug fix From 30e4fcc8e298f93bed84ad4541fd5cb49f143ded Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 5 Nov 2021 11:08:10 +0100 Subject: [PATCH 38/39] Apply suggestions from code review Co-authored-by: Hajar AIT EL KADI <48837850+HAEKADI@users.noreply.github.com> --- openfisca_core/entities/__init__.py | 4 ++-- openfisca_core/entities/_variable_proxy.py | 2 +- openfisca_core/entities/tests/test_variable_proxy.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index 3c43da7e8f..57cdc7d220 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -26,8 +26,8 @@ and in the latter case, the way the rule is going to be applied depends on the attributes and roles of the members of the group. -Finally, there is a distiction to be made between the "abstract" entities -described by in a rule system, for example an individual, as in "any" +Finally, there is a distinction to be made between the "abstract" entities +described in a rule system, for example an individual, as in "any" individual, and an actual individual, like Mauko, Andrea, Mehdi, Seiko, or José. diff --git a/openfisca_core/entities/_variable_proxy.py b/openfisca_core/entities/_variable_proxy.py index cbae01007f..0d5c8948c1 100644 --- a/openfisca_core/entities/_variable_proxy.py +++ b/openfisca_core/entities/_variable_proxy.py @@ -138,7 +138,7 @@ def exists(self) -> _VariableProxy: def isdefined(self) -> _VariableProxy: """Checks that ``variable_name`` is defined for :attr:`.entity`.""" - # We assume that we're also checking for existance. + # We assume that we're also checking for existence. self.exists() self.query = functools.partial( diff --git a/openfisca_core/entities/tests/test_variable_proxy.py b/openfisca_core/entities/tests/test_variable_proxy.py index e37f31aa21..ae6c720dd2 100644 --- a/openfisca_core/entities/tests/test_variable_proxy.py +++ b/openfisca_core/entities/tests/test_variable_proxy.py @@ -95,7 +95,7 @@ def test_variables_when_doesnt_exist(entity, tbs): def test_variables_when_exists_and_check_exists(entity, tbs, ThisVar): - """Raises a VariableNotFoundError when checking for existance.""" + """Raises a VariableNotFoundError when checking for existence.""" entity.tax_benefit_system = tbs variable = entity.variables.exists().get("ThisVar") @@ -103,7 +103,7 @@ def test_variables_when_exists_and_check_exists(entity, tbs, ThisVar): def test_variables_when_doesnt_exist_and_check_exists(entity, tbs): - """Raises a VariableNotFoundError when checking for existance.""" + """Raises a VariableNotFoundError when checking for existence.""" entity.tax_benefit_system = tbs From 5f4372ec0d9cd1898a42985a27ef4a5fe6a39d5a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 13 Nov 2021 04:20:25 +0100 Subject: [PATCH 39/39] Apply suggestions from code review Co-authored-by: Matti Schneider --- 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 a4ec2f686d..122a030c8c 100644 --- a/openfisca_core/entities/tests/test_entity.py +++ b/openfisca_core/entities/tests/test_entity.py @@ -2,7 +2,7 @@ def test_init_when_doc_indented(): - """Dedents the ``doc`` attribute if it is passed at initialisation.""" + """Unindents 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 index aa7c03f8a5..c4530efc14 100644 --- a/openfisca_core/entities/tests/test_group_entity.py +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -18,7 +18,7 @@ def group_entity(roles): def test_init_when_doc_indented(): - """Dedents the ``doc`` attribute if it is passed at initialisation.""" + """Unindents the ``doc`` attribute if it is passed at initialisation.""" key = "\tkey" doc = "\tdoc" diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py index 0953a8689c..8a24b3f61e 100644 --- a/openfisca_core/entities/tests/test_role.py +++ b/openfisca_core/entities/tests/test_role.py @@ -2,7 +2,7 @@ def test_init_when_doc_indented(): - """Dedents the ``doc`` attribute if it is passed at initialisation.""" + """Unindents the ``doc`` attribute if it is passed at initialisation.""" key = "\tkey" doc = "\tdoc"