From 9f2242f8abf43dc127874be1348e359d9496c49d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 1 Oct 2024 04:29:17 +0200 Subject: [PATCH 1/3] test(entities): fix doctests (#1254) --- openfisca_core/entities/__init__.py | 67 ++++++++----- openfisca_core/entities/_core_entity.py | 119 ++++++++++++++++++++---- openfisca_core/entities/entity.py | 23 ++++- openfisca_core/entities/group_entity.py | 47 +++++++++- openfisca_core/entities/helpers.py | 26 +++--- openfisca_core/entities/role.py | 3 +- setup.cfg | 22 ++--- 7 files changed, 238 insertions(+), 69 deletions(-) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index 9546773cb..43cbd2a8f 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -1,40 +1,61 @@ -# 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 +"""Provide 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 individual, and payroll taxes by a company, as a juridical person. In +OpenFisca, that atomic element is represented as an ``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 ``GroupEntity``. + +In the latter case, the atomic members of a given group may have a different +``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 ``Variable`` +is defined either for an ``.Entity`` or for a ``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 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é. + +This module provides tools for modelling the former. For the actual +"simulation" or "application" of any given ``Variable`` to a +concrete individual or group of individuals, see ``Population`` +and ``GroupPopulation``. + +""" from . import types +from ._core_entity import CoreEntity from .entity import Entity from .group_entity import GroupEntity from .helpers import build_entity, find_role from .role import Role SingleEntity = Entity +check_role_validity = CoreEntity.check_role_validity __all__ = [ + "CoreEntity", "Entity", - "SingleEntity", "GroupEntity", "Role", + "SingleEntity", "build_entity", + "check_role_validity", "find_role", "types", ] diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index f44353e11..3ca87416e 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -9,19 +9,27 @@ from .role import Role -class _CoreEntity: +class CoreEntity: """Base class to build entities from. Args: - __key: A key to identify the ``_CoreEntity``. - __plural: The ``key`` pluralised. - __label: A summary description. - __doc: A full description. - *__args: Additional arguments. + *__args: Any arguments. + **__kwargs: Any keyword arguments. + + Examples: + >>> from openfisca_core import entities + >>> from openfisca_core.entities import types as t + + >>> class Entity(entities.CoreEntity): + ... def __init__(self, key): + ... self.key = t.EntityKey(key) + + >>> Entity("individual") + Entity(individual) """ - #: A key to identify the ``_CoreEntity``. + #: A key to identify the ``CoreEntity``. key: t.EntityKey #: The ``key`` pluralised. @@ -33,27 +41,20 @@ class _CoreEntity: #: A full description. doc: str - #: Whether the ``_CoreEntity`` is a person or not. + #: Whether the ``CoreEntity`` is a person or not. is_person: ClassVar[bool] #: A ``TaxBenefitSystem`` instance. _tax_benefit_system: None | t.TaxBenefitSystem = None @abc.abstractmethod - def __init__( - self, - __key: str, - __plural: str, - __label: str, - __doc: str, - *__args: object, - ) -> None: ... + def __init__(self, *__args: object, **__kwargs: object) -> None: ... def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key})" def set_tax_benefit_system(self, tax_benefit_system: t.TaxBenefitSystem) -> None: - """A ``_CoreEntity`` belongs to a ``TaxBenefitSystem``.""" + """A ``CoreEntity`` belongs to a ``TaxBenefitSystem``.""" self._tax_benefit_system = tax_benefit_system def get_variable( @@ -72,9 +73,45 @@ def get_variable( None: When the ``Variable`` doesn't exist. Raises: + ValueError: When the :attr:`_tax_benefit_system` is not set yet. ValueError: When ``check_existence`` is ``True`` and the ``Variable`` doesn't exist. + Examples: + >>> from openfisca_core import ( + ... entities, + ... periods, + ... taxbenefitsystems, + ... variables, + ... ) + + >>> this = entities.SingleEntity("this", "", "", "") + >>> that = entities.SingleEntity("that", "", "", "") + + >>> this.get_variable("tax") + Traceback (most recent call last): + ValueError: You must set 'tax_benefit_system' before calling thi... + + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem([this]) + >>> this.set_tax_benefit_system(tax_benefit_system) + + >>> this.get_variable("tax") + + >>> this.get_variable("tax", check_existence=True) + Traceback (most recent call last): + VariableNotFoundError: You tried to calculate or to set a value... + + >>> class tax(variables.Variable): + ... definition_period = periods.MONTH + ... value_type = float + ... entity = that + + >>> this._tax_benefit_system.add_variable(tax) + + + >>> this.get_variable("tax") + + """ if self._tax_benefit_system is None: @@ -98,6 +135,42 @@ def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> No ValueError: When the ``Variable`` exists but is defined for another ``Entity``. + Examples: + >>> from openfisca_core import ( + ... entities, + ... periods, + ... taxbenefitsystems, + ... variables, + ... ) + + >>> this = entities.SingleEntity("this", "", "", "") + >>> that = entities.SingleEntity("that", "", "", "") + >>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem([that]) + >>> this.set_tax_benefit_system(tax_benefit_system) + + >>> this.check_variable_defined_for_entity("tax") + Traceback (most recent call last): + VariableNotFoundError: You tried to calculate or to set a value... + + >>> class tax(variables.Variable): + ... definition_period = periods.WEEK + ... value_type = int + ... entity = that + + >>> this._tax_benefit_system.add_variable(tax) + + + >>> this.check_variable_defined_for_entity("tax") + Traceback (most recent call last): + ValueError: You tried to compute the variable 'tax' for the enti... + + >>> tax.entity = this + + >>> this._tax_benefit_system.update_variable(tax) + + + >>> this.check_variable_defined_for_entity("tax") + """ entity: None | t.CoreEntity = None @@ -132,6 +205,16 @@ def check_role_validity(role: object) -> None: Raises: ValueError: When ``role`` is not a ``Role``. + Examples: + >>> from openfisca_core import entities + + >>> role = entities.Role({"key": "key"}, object()) + >>> entities.check_role_validity(role) + + >>> entities.check_role_validity("hey!") + Traceback (most recent call last): + ValueError: hey! is not a valid role + """ if role is not None and not isinstance(role, Role): @@ -139,4 +222,4 @@ def check_role_validity(role: object) -> None: raise ValueError(msg) -__all__ = ["_CoreEntity"] +__all__ = ["CoreEntity"] diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index c00191816..56eb816c2 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -3,10 +3,10 @@ import textwrap from . import types as t -from ._core_entity import _CoreEntity +from ._core_entity import CoreEntity -class Entity(_CoreEntity): +class Entity(CoreEntity): """An entity (e.g. a person, a household) on which calculations can be run. Args: @@ -15,6 +15,25 @@ class Entity(_CoreEntity): label: A summary description. doc: A full description. + Examples: + >>> from openfisca_core import entities + + >>> entity = entities.SingleEntity( + ... "individual", + ... "individuals", + ... "An individual", + ... "\t\t\tThe minimal legal entity on which a rule might be a...", + ... ) + + >>> repr(entities.SingleEntity) + "" + + >>> repr(entity) + 'Entity(individual)' + + >>> str(entity) + 'Entity(individual)' + """ #: A key to identify the ``Entity``. diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index 4b588567a..ea8bf1581 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -7,11 +7,11 @@ from itertools import chain from . import types as t -from ._core_entity import _CoreEntity +from ._core_entity import CoreEntity from .role import Role -class GroupEntity(_CoreEntity): +class GroupEntity(CoreEntity): """Represents an entity containing several others with different roles. A ``GroupEntity`` represents an ``Entity`` containing several other entities, @@ -26,6 +26,49 @@ class GroupEntity(_CoreEntity): containing_entities: The list of keys of group entities whose members are guaranteed to be a superset of this group's entities. + Examples: + >>> from openfisca_core import entities + + >>> family_roles = [ + ... { + ... "key": "parent", + ... "subroles": ["first_parent", "second_parent"], + ... } + ... ] + + >>> family = entities.GroupEntity( + ... "family", + ... "families", + ... "A family", + ... "\t\t\tAll the people somehow related living together.", + ... family_roles, + ... ) + + >>> household_roles = [ + ... { + ... "key": "partners", + ... "subroles": ["first_partner", "second_partner"], + ... } + ... ] + + >>> household = entities.GroupEntity( + ... "household", + ... "households", + ... "A household", + ... "\t\t\tAll the people who live together in the same place.", + ... household_roles, + ... (family.key,), + ... ) + + >>> repr(entities.GroupEntity) + "" + + >>> repr(household) + 'GroupEntity(household)' + + >>> str(household) + 'GroupEntity(household)' + """ #: A key to identify the ``Entity``. diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 146ab6d25..1ed3c71b0 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -40,7 +40,7 @@ def build_entity( Examples: >>> from openfisca_core import entities - >>> entity = build_entity( + >>> entity = entities.build_entity( ... "syndicate", ... "syndicates", ... "Banks loaning jointly.", @@ -50,7 +50,7 @@ def build_entity( >>> entity GroupEntity(syndicate) - >>> build_entity( + >>> entities.build_entity( ... "company", ... "companies", ... "A small or medium company.", @@ -60,7 +60,7 @@ def build_entity( >>> role = entities.Role({"key": "key"}, entity) - >>> build_entity( + >>> entities.build_entity( ... "syndicate", ... "syndicates", ... "Banks loaning jointly.", @@ -105,23 +105,24 @@ def find_role( None: Else ``None``. Examples: - >>> from openfisca_core.entities.types import RoleParams + >>> from openfisca_core import entities + >>> from openfisca_core.entities import types as t - >>> principal = RoleParams( + >>> principal = t.RoleParams( ... key="principal", ... label="Principal", ... doc="Person focus of a calculation in a family context.", ... max=1, ... ) - >>> partner = RoleParams( + >>> partner = t.RoleParams( ... key="partner", ... plural="partners", ... label="Partners", ... doc="Persons partners of the principal.", ... ) - >>> parent = RoleParams( + >>> parent = t.RoleParams( ... key="parent", ... plural="parents", ... label="Parents", @@ -129,7 +130,7 @@ def find_role( ... subroles=["first_parent", "second_parent"], ... ) - >>> group_entity = build_entity( + >>> group_entity = entities.build_entity( ... key="family", ... plural="families", ... label="Family", @@ -137,19 +138,20 @@ def find_role( ... roles=[principal, partner, parent], ... ) - >>> find_role(group_entity.roles, "principal", total=1) + >>> entities.find_role(group_entity.roles, "principal", total=1) Role(principal) - >>> find_role(group_entity.roles, "partner") + >>> entities.find_role(group_entity.roles, "partner") Role(partner) - >>> find_role(group_entity.roles, "parent", total=2) + >>> entities.find_role(group_entity.roles, "parent", total=2) Role(parent) - >>> find_role(group_entity.roles, "first_parent", total=1) + >>> entities.find_role(group_entity.roles, "first_parent", total=1) Role(first_parent) """ + for role in roles: if role.subroles: for subrole in role.subroles: diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index e687b2604..39bd5090e 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -20,10 +20,11 @@ class Role: Examples: >>> from openfisca_core import entities + >>> entity = entities.GroupEntity("key", "plural", "label", "doc", []) >>> role = entities.Role({"key": "parent"}, entity) - >>> repr(Role) + >>> repr(entities.Role) "" >>> repr(role) diff --git a/setup.cfg b/setup.cfg index 60ac8faf0..23760bcce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,17 +12,17 @@ convention = google docstring_style = google extend-ignore = D ignore = - B019, - E203, - E501, - F405, - E701, - E704, - RST210, - RST212, - RST213, - RST301, - RST306, + B019 + E203 + E501 + F405 + E701 + E704 + RST210 + RST212 + RST213 + RST301 + RST306 W503 in-place = true include-in-doctest = From 8d8d31adfcf97a93d2573b88c09da8e078a96429 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 1 Oct 2024 04:34:37 +0200 Subject: [PATCH 2/3] chore: version bump --- CHANGELOG.md | 11 +++++++++++ setup.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 549588caf..9b866fe9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## 43.1.0 [#1255](https://github.com/openfisca/openfisca-core/pull/1255) + +#### New features + +- Make `CoreEntity` public + - Allows for more easily creating customised entities. + +#### Technical changes + +- Add missing doctests. + # 43.0.0 [#1224](https://github.com/openfisca/openfisca-core/pull/1224) #### Technical changes diff --git a/setup.py b/setup.py index d20cd6bb8..64fb1fe71 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup( name="OpenFisca-Core", - version="43.0.0", + version="43.1.0", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[ From 233df9ebb8eef7886b167604148859e4b8e5de6c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 11 Oct 2024 00:00:53 +0200 Subject: [PATCH 3/3] chore(entities): cleanup (#1254) --- openfisca_core/entities/__init__.py | 40 +------------------------ openfisca_core/entities/_core_entity.py | 7 ----- openfisca_core/entities/_description.py | 2 +- openfisca_core/entities/entity.py | 2 +- openfisca_core/entities/group_entity.py | 2 +- openfisca_core/entities/helpers.py | 2 -- 6 files changed, 4 insertions(+), 51 deletions(-) diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index 43cbd2a8f..1811e3fe9 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -1,42 +1,4 @@ -"""Provide 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 individual, and payroll taxes by a company, as a juridical person. In -OpenFisca, that atomic element is represented as an ``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 ``GroupEntity``. - -In the latter case, the atomic members of a given group may have a different -``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 ``Variable`` -is defined either for an ``.Entity`` or for a ``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 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é. - -This module provides tools for modelling the former. For the actual -"simulation" or "application" of any given ``Variable`` to a -concrete individual or group of individuals, see ``Population`` -and ``GroupPopulation``. - -""" +"""Provide a way of representing the entities of a rule system.""" from . import types from ._core_entity import CoreEntity diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index 3ca87416e..83af037b8 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -113,7 +113,6 @@ def get_variable( """ - if self._tax_benefit_system is None: msg = "You must set 'tax_benefit_system' before calling this method." raise ValueError( @@ -127,10 +126,6 @@ def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> No Args: variable_name: The ``Variable`` to be found. - Returns: - Variable: When the ``Variable`` exists. - None: When the :attr:`_tax_benefit_system` is not set. - Raises: ValueError: When the ``Variable`` exists but is defined for another ``Entity``. @@ -172,7 +167,6 @@ def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> No >>> this.check_variable_defined_for_entity("tax") """ - entity: None | t.CoreEntity = None variable: None | t.Variable = self.get_variable( variable_name, @@ -216,7 +210,6 @@ def check_role_validity(role: object) -> None: ValueError: hey! is not a valid role """ - if role is not None and not isinstance(role, Role): msg = f"{role} is not a valid role" raise ValueError(msg) diff --git a/openfisca_core/entities/_description.py b/openfisca_core/entities/_description.py index 78634ca27..6e2d68af1 100644 --- a/openfisca_core/entities/_description.py +++ b/openfisca_core/entities/_description.py @@ -6,7 +6,7 @@ @dataclasses.dataclass(frozen=True) class _Description: - """A ``Role``'s description. + r"""A ``Role``'s description. Examples: >>> data = { diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 56eb816c2..673aae48b 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -7,7 +7,7 @@ class Entity(CoreEntity): - """An entity (e.g. a person, a household) on which calculations can be run. + r"""An entity (e.g. a person, a household) on which calculations can be run. Args: key: A key to identify the ``Entity``. diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index ea8bf1581..796da105e 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -12,7 +12,7 @@ class GroupEntity(CoreEntity): - """Represents an entity containing several others with different roles. + r"""Represents an entity containing several others with different roles. A ``GroupEntity`` represents an ``Entity`` containing several other entities, with different roles, and on which calculations can be run. diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 1ed3c71b0..1dcdad88a 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -70,7 +70,6 @@ def build_entity( TypeError: 'Role' object is not subscriptable """ - if is_person: return SingleEntity(key, plural, label, doc) @@ -151,7 +150,6 @@ def find_role( Role(first_parent) """ - for role in roles: if role.subroles: for subrole in role.subroles: