Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING CHANGE: cleanup entities #1286

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# Changelog

# 44.0.0 [#1286](https://github.com/openfisca/openfisca-core/pull/1286)

#### New features

- Add `CoreEntity.variables`
- Allows for querying the variables defined for an entity.

#### Deprecations

- Remove `CoreEntity.check_role_validity`
- The method in itself had no relation to `entities` (it was a static
method, so no depending neither on an entity class nor an instance). It
could have been kept as a `populations` helper, where it was exclusively
used. Nonetheless, `check_role_validity` had the peculiar behaviour of
not failing but when the value of `role` is not `None`, that granted it
in the author's opinion the right to be removed so as to avoid confusion.
- Deprecate `class_override` arg from `entities.build_entity`
- That argument actually did nothing, and linters have been complaining
about this argument for almost 10 years now. It is only being marked as
a breaking change for respect to the semantic versioning protocol.
- Deprecate `CoreEntity.check_variable_defined_for_entity`
- The feature is still provided by `CoreEntity.get_variable` when passed
the optional positional or keyword argument `check_existence`. In fact,
the removed method was duplicating and already existing behaviour.
- Deprecate `CoreEntity.set_tax_benefit_system`
- The feature is now provided by simple variable assignment:
```python
entity.tax_benefit_system = tax_benefit_system
```
- Likewise, `entity.tax_benefit_system` becomes part of the public API
(which implicitly it was as the private marker was being violated).

### 43.2.5 [#1296](https://github.com/openfisca/openfisca-core/pull/1296)

#### Bugfix
Expand Down
5 changes: 3 additions & 2 deletions openfisca_core/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@

from . import types
from ._core_entity import CoreEntity
from ._errors import TaxBenefitSystemUnsetError, VariableNotFoundError
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",
"GroupEntity",
"Role",
"SingleEntity",
"TaxBenefitSystemUnsetError",
"VariableNotFoundError",
"build_entity",
"check_role_validity",
"find_role",
"types",
]
180 changes: 74 additions & 106 deletions openfisca_core/entities/_core_entity.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import ClassVar

import abc
import os

from . import types as t
from .role import Role
from ._errors import TaxBenefitSystemUnsetError, VariableNotFoundError


class CoreEntity:
Expand Down Expand Up @@ -45,37 +45,24 @@ class CoreEntity:
is_person: ClassVar[bool]

#: A ``TaxBenefitSystem`` instance.
_tax_benefit_system: None | t.TaxBenefitSystem = None
tax_benefit_system: None | t.TaxBenefitSystem = None

@abc.abstractmethod
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``."""
self._tax_benefit_system = tax_benefit_system

def get_variable(
self,
variable_name: t.VariableName,
check_existence: bool = False,
) -> t.Variable | None:
"""Get ``variable_name`` from ``variables``.

Args:
variable_name: The ``Variable`` to be found.
check_existence: Was the ``Variable`` found?
@property
def variables(self, /) -> Mapping[t.VariableName, t.Variable]:
"""Get all variables defined for the entity.

Returns:
Variable: When the ``Variable`` exists.
None: When the ``Variable`` doesn't exist.
dict[str, Variable]: The variables defined for the entity.

Raises:
ValueError: When the :attr:`_tax_benefit_system` is not set yet.
ValueError: When ``check_existence`` is ``True`` and
the ``Variable`` doesn't exist.
TaxBenefitSystemUnsetError: When the :attr:`.tax_benefit_system` is
not set yet.

Examples:
>>> from openfisca_core import (
Expand All @@ -85,50 +72,69 @@ def get_variable(
... variables,
... )

>>> this = entities.SingleEntity("this", "", "", "")
>>> that = entities.SingleEntity("that", "", "", "")
>>> this = entities.SingleEntity("this", "these", "", "")
>>> that = entities.SingleEntity("that", "those", "", "")

>>> this.get_variable("tax")
>>> this.variables
Traceback (most recent call last):
ValueError: You must set 'tax_benefit_system' before calling this...
TaxBenefitSystemUnsetError: The tax and benefit system is not se...

>>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem([this])
>>> this.set_tax_benefit_system(tax_benefit_system)
>>> tbs = taxbenefitsystems.TaxBenefitSystem([this, that])
>>> this, that = tbs.entities

>>> this.get_variable("tax")
>>> this.variables
{}

>>> this.get_variable("tax", check_existence=True)
Traceback (most recent call last):
VariableNotFoundError: You tried to calculate or to set a value...
>>> that.variables
{}

>>> class tax(variables.Variable):
... definition_period = periods.MONTH
... value_type = float
... entity = that

>>> this._tax_benefit_system.add_variable(tax)
>>> tbs.add_variable(tax)
<openfisca_core.entities._core_entity.tax object at ...>

>>> this.get_variable("tax")
>>> this.variables
{}

>>> that.variables
{'tax': <openfisca_core.entities._core_entity.tax object at ...>}

>>> that.variables["tax"]
<openfisca_core.entities._core_entity.tax object at ...>

"""
if self._tax_benefit_system is None:
msg = "You must set 'tax_benefit_system' before calling this method."
raise ValueError(
msg,
)
return self._tax_benefit_system.get_variable(variable_name, check_existence)
if self.tax_benefit_system is None:
raise TaxBenefitSystemUnsetError
return {
name: variable
for name, variable in self.tax_benefit_system.variables.items()
if variable.entity.key == self.key
}

def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> None:
"""Check if ``variable_name`` is defined for ``self``.
def get_variable(
self,
variable_name: t.VariableName,
/,
check_existence: bool = False,
) -> None | t.Variable:
"""Get ``variable_name`` from ``variables``.

Args:
variable_name: The ``Variable`` to be found.
check_existence: Was the ``Variable`` found?

Returns:
Variable: When the ``Variable`` exists.
None: When the ``Variable`` doesn't exist.

Raises:
ValueError: When the ``Variable`` exists but is defined
for another ``Entity``.
TaxBenefitSystemUnsetError: When the :attr:`.tax_benefit_system` is
not set yet.
VariableNotFoundError: When ``check_existence`` is ``True`` and the
``Variable`` doesn't exist.

Examples:
>>> from openfisca_core import (
Expand All @@ -138,81 +144,43 @@ def check_variable_defined_for_entity(self, variable_name: t.VariableName) -> No
... variables,
... )

>>> this = entities.SingleEntity("this", "", "", "")
>>> that = entities.SingleEntity("that", "", "", "")
>>> tax_benefit_system = taxbenefitsystems.TaxBenefitSystem([that])
>>> this.set_tax_benefit_system(tax_benefit_system)
>>> this = entities.SingleEntity("this", "these", "", "")
>>> that = entities.SingleEntity("that", "those", "", "")

>>> this.check_variable_defined_for_entity("tax")
>>> this.get_variable("tax")
Traceback (most recent call last):
VariableNotFoundError: You tried to calculate or to set a value...
TaxBenefitSystemUnsetError: The tax and benefit system is not se...

>>> class tax(variables.Variable):
... definition_period = periods.WEEK
... value_type = int
... entity = that
>>> tbs = taxbenefitsystems.TaxBenefitSystem([this, that])
>>> this, that = tbs.entities

>>> this._tax_benefit_system.add_variable(tax)
<openfisca_core.entities._core_entity.tax object at ...>
>>> this.get_variable("tax")

>>> this.check_variable_defined_for_entity("tax")
>>> this.get_variable("tax", check_existence=True)
Traceback (most recent call last):
ValueError: You tried to compute the variable 'tax' for the enti...
VariableNotFoundError: You requested the variable 'tax', but it ...

>>> tax.entity = this
>>> class tax(variables.Variable):
... definition_period = periods.MONTH
... value_type = float
... entity = that

>>> this._tax_benefit_system.update_variable(tax)
>>> tbs.add_variable(tax)
<openfisca_core.entities._core_entity.tax object at ...>

>>> this.check_variable_defined_for_entity("tax")

"""
entity: None | t.CoreEntity = None
variable: None | t.Variable = self.get_variable(
variable_name,
check_existence=True,
)

if variable is not None:
entity = variable.entity

if entity is None:
return

if entity.key != self.key:
message = (
f"You tried to compute the variable '{variable_name}' for",
f"the entity '{self.plural}'; however the variable",
f"'{variable_name}' is defined for '{entity.plural}'.",
"Learn more about entities in our documentation:",
"<https://openfisca.org/doc/coding-the-legislation/50_entities.html>.",
)
raise ValueError(os.linesep.join(message))

@staticmethod
def check_role_validity(role: object) -> None:
"""Check if ``role`` is an instance of ``Role``.

Args:
role: Any object.

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)
>>> this.get_variable("tax")

>>> entities.check_role_validity("hey!")
Traceback (most recent call last):
ValueError: hey! is not a valid role
>>> that.get_variable("tax")
<openfisca_core.entities._core_entity.tax object at ...>

"""
if role is not None and not isinstance(role, Role):
msg = f"{role} is not a valid role"
raise ValueError(msg)
if self.tax_benefit_system is None:
raise TaxBenefitSystemUnsetError
if (variable := self.variables.get(variable_name)) is not None:
return variable
if check_existence:
raise VariableNotFoundError(variable_name, self.plural)
return None


__all__ = ["CoreEntity"]
32 changes: 32 additions & 0 deletions openfisca_core/entities/_errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from . import types as t


class TaxBenefitSystemUnsetError(ValueError):
"""Raised when a tax and benefit system is not set yet."""

def __init__(self) -> None:
msg = (
"The tax and benefit system is not set yet. You need to set it "
"before using this method: `entity.tax_benefit_system = ...`."
)
super().__init__(msg)


class VariableNotFoundError(ValueError):
"""Raised when a requested variable is not defined for as entity."""

def __init__(self, name: t.VariableName, plural: t.EntityPlural) -> None:
msg = (
f"You requested the variable '{name}', but it was not found in "
f"the entity '{plural}'. Are you sure you spelled '{name}' "
"correctly? If this code used to work and suddenly does not, "
"this is most probably linked to an update of the tax and "
"benefit system. Look at its CHANGELOG to learn about renames "
"and removals and update your code accordingly."
"Learn more about entities in our documentation:"
"<https://openfisca.org/doc/coding-the-legislation/50_entities.html>."
)
super().__init__(msg)


__all__ = ["TaxBenefitSystemUnsetError", "VariableNotFoundError"]
2 changes: 0 additions & 2 deletions openfisca_core/entities/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ def build_entity(
roles: None | Sequence[t.RoleParams] = None,
is_person: bool = False,
*,
class_override: object = None,
containing_entities: Sequence[str] = (),
) -> t.SingleEntity | t.GroupEntity:
"""Build an ``Entity`` or a ``GroupEntity``.
Expand All @@ -27,7 +26,6 @@ def build_entity(
doc: A full description.
roles: A list of roles —if it's a ``GroupEntity``.
is_person: If is an individual, or not.
class_override: ?
containing_entities: Keys of contained entities.

Returns:
Expand Down
Loading
Loading