Skip to content

Commit

Permalink
Add tests to entities (#1192)
Browse files Browse the repository at this point in the history
  • Loading branch information
bonjourmauko committed Oct 3, 2023
2 parents 23979ed + 696e00f commit a3c2f83
Show file tree
Hide file tree
Showing 14 changed files with 200 additions and 87 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# Changelog

### 40.0.2 [#1186](https://github.com/openfisca/openfisca-core/pull/1186)
### 41.1.2 [#1192](https://github.com/openfisca/openfisca-core/pull/1192)

#### Technical changes

- Add tests to `entities`.

### 41.1.1 [#1186](https://github.com/openfisca/openfisca-core/pull/1186)

#### Technical changes

Expand Down
11 changes: 7 additions & 4 deletions openfisca_core/entities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
#
# See: https://www.python.org/dev/peps/pep-0008/#imports

from .entity import Entity # noqa: F401
from .group_entity import GroupEntity # noqa: F401
from .helpers import build_entity # noqa: F401
from .role import Role # noqa: F401
from . import typing
from .entity import Entity
from .group_entity import GroupEntity
from .helpers import build_entity
from .role import Role

__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "typing"]
23 changes: 9 additions & 14 deletions openfisca_core/entities/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class Entity:
Represents an entity (e.g. a person, a household, etc.) on which calculations can be run.
"""

def __init__(self, key, plural, label, doc):
def __init__(self, key: str, plural: str, label: str, doc: str) -> None:
self.key = key
self.label = label
self.plural = plural
Expand All @@ -25,7 +25,7 @@ def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystem):

def check_role_validity(self, role: Any) -> None:
if role is not None and not isinstance(role, Role):
raise ValueError("{} is not a valid role".format(role))
raise ValueError(f"{role} is not a valid role")

def get_variable(
self,
Expand All @@ -44,16 +44,11 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None:
entity = variable.entity

if entity.key != self.key:
message = os.linesep.join(
[
"You tried to compute the variable '{0}' for the entity '{1}';".format(
variable_name, self.plural
),
"however the variable '{0}' is defined for '{1}'.".format(
variable_name, entity.plural
),
"Learn more about entities in our documentation:",
"<https://openfisca.org/doc/coding-the-legislation/50_entities.html>.",
]
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(message)
raise ValueError(os.linesep.join(message))
24 changes: 17 additions & 7 deletions openfisca_core/entities/group_entity.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from collections.abc import Iterable, Mapping
from typing import Any

from itertools import chain

from .entity import Entity
from .role import Role

Expand All @@ -18,13 +23,17 @@ class GroupEntity(Entity):
containing_entities: The list of keys of group entities whose members
are guaranteed to be a superset of this group's entities.
.. versionchanged:: 35.7.0
Added ``containing_entities``, that allows the defining of group
entities which entirely contain other group entities.
""" # noqa RST301

def __init__(self, key, plural, label, doc, roles, containing_entities=()):
def __init__(
self,
key: str,
plural: str,
label: str,
doc: str,
roles: Iterable[Mapping[str, Any]],
containing_entities: Iterable[str] = (),
) -> None:
super().__init__(key, plural, label, doc)
self.roles_description = roles
self.roles = []
Expand All @@ -39,8 +48,9 @@ def __init__(self, key, plural, label, doc, roles, containing_entities=()):
setattr(self, subrole.key.upper(), subrole)
role.subroles.append(subrole)
role.max = len(role.subroles)
self.flattened_roles = sum(
[role2.subroles or [role2] for role2 in self.roles], []
self.flattened_roles = tuple(
chain.from_iterable(role.subroles or [role] for role in self.roles)
)

self.is_person = False
self.containing_entities = containing_entities
7 changes: 4 additions & 3 deletions openfisca_core/entities/helpers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from openfisca_core import entities
from .entity import Entity
from .group_entity import GroupEntity


def build_entity(
Expand All @@ -12,8 +13,8 @@ def build_entity(
containing_entities=(),
):
if is_person:
return entities.Entity(key, plural, label, doc)
return Entity(key, plural, label, doc)
else:
return entities.GroupEntity(
return GroupEntity(
key, plural, label, doc, roles, containing_entities=containing_entities
)
95 changes: 60 additions & 35 deletions openfisca_core/entities/role.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from collections.abc import Mapping, Sequence
from typing import Any

import dataclasses
Expand All @@ -17,11 +18,8 @@ class Role:
several dependents, and the like.
Attributes:
entity (Entity): The Entity to which the Role belongs.
key (str): A key to identify the Role.
plural (str): The ``key``, pluralised.
label (str): A summary description.
doc (str): A full description, dedented.
entity (Entity): The Entity the Role belongs to.
description (_Description): A description of the Role.
max (int): Max number of members.
subroles (list[Role]): A list of subroles.
Expand Down Expand Up @@ -49,48 +47,80 @@ class Role:
"""

def __init__(self, description: dict[str, Any], entity: Entity) -> None:
role_description: _RoleDescription = _RoleDescription(**description)
self.entity: Entity = entity
self.key: str = role_description.key
self.plural: str | None = role_description.plural
self.label: str | None = role_description.label
self.doc: str = role_description.doc
self.max: int | None = role_description.max
self.subroles: list[Role] | None = None
#: The Entity the Role belongs to.
entity: Entity

#: A description of the Role.
description: _Description

#: Max number of members.
max: int | None = None

#: A list of subroles.
subroles: Sequence[Role] | None = None

@property
def key(self) -> str:
"""A key to identify the Role."""
return self.description.key

@property
def plural(self) -> str | None:
"""The ``key``, pluralised."""
return self.description.plural

@property
def label(self) -> str | None:
"""A summary description."""
return self.description.label

@property
def doc(self) -> str | None:
"""A full description, non-indented."""
return self.description.doc

def __init__(self, description: Mapping[str, Any], entity: Entity) -> None:
self.description = _Description(
**{
key: value
for key, value in description.items()
if key in {"key", "plural", "label", "doc"}
}
)
self.entity = entity
self.max = description.get("max")

def __repr__(self) -> str:
return f"Role({self.key})"


@dataclasses.dataclass(frozen=True)
class _RoleDescription:
class _Description:
"""A Role's description.
Examples:
>>> description = {
>>> data = {
... "key": "parent",
... "label": "Parents",
... "plural": "parents",
... "doc": "\t\t\tThe one/two adults in charge of the household.",
... "max": 2,
... }
>>> role_description = _RoleDescription(**description)
>>> description = _Description(**data)
>>> repr(_RoleDescription)
"<class 'openfisca_core.entities.role._RoleDescription'>"
>>> repr(_Description)
"<class 'openfisca_core.entities.role._Description'>"
>>> repr(role_description)
"_RoleDescription(key='parent', plural='parents', label='Parents',...)"
>>> repr(description)
"_Description(key='parent', plural='parents', label='Parents', ...)"
>>> str(role_description)
"_RoleDescription(key='parent', plural='parents', label='Parents',...)"
>>> str(description)
"_Description(key='parent', plural='parents', label='Parents', ...)"
>>> {role_description}
{...}
>>> {description}
{_Description(key='parent', plural='parents', label='Parents', doc=...}
>>> role_description.key
>>> description.key
'parent'
.. versionadded:: 41.0.1
Expand All @@ -107,13 +137,8 @@ class _RoleDescription:
label: str | None = None

#: A full description, non-indented.
doc: str = ""

#: Max number of members.
max: int | None = None

#: A list of subroles.
subroles: list[str] | None = None
doc: str | None = None

def __post_init__(self) -> None:
object.__setattr__(self, "doc", textwrap.dedent(self.doc))
if self.doc is not None:
object.__setattr__(self, "doc", textwrap.dedent(self.doc))
Empty file.
11 changes: 11 additions & 0 deletions openfisca_core/entities/tests/test_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from openfisca_core import entities


def test_init_when_doc_indented() -> None:
"""De-indent the ``doc`` attribute if it is passed at initialisation."""

key = "\tkey"
doc = "\tdoc"
entity = entities.Entity(key, "label", "plural", doc)
assert entity.key == key
assert entity.doc == doc.lstrip()
69 changes: 69 additions & 0 deletions openfisca_core/entities/tests/test_group_entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from collections.abc import Mapping
from typing import Any

import pytest

from openfisca_core import entities


@pytest.fixture
def parent() -> str:
return "parent"


@pytest.fixture
def uncle() -> str:
return "uncle"


@pytest.fixture
def first_parent() -> str:
return "first_parent"


@pytest.fixture
def second_parent() -> str:
return "second_parent"


@pytest.fixture
def third_parent() -> str:
return "third_parent"


@pytest.fixture
def role(parent: str, first_parent: str, third_parent: str) -> Mapping[str, Any]:
return {"key": parent, "subroles": {first_parent, third_parent}}


@pytest.fixture
def group_entity(role: Mapping[str, Any]) -> entities.GroupEntity:
return entities.GroupEntity("key", "label", "plural", "doc", (role,))


def test_init_when_doc_indented() -> None:
"""De-indent the ``doc`` attribute if it is passed at initialisation."""

key = "\tkey"
doc = "\tdoc"
group_entity = entities.GroupEntity(key, "label", "plural", doc, ())
assert group_entity.key == key
assert group_entity.doc == doc.lstrip()


def test_group_entity_with_roles(
group_entity: entities.GroupEntity, parent: str, uncle: str
) -> None:
"""Assign a Role for each role-like passed as argument."""

assert hasattr(group_entity, parent.upper())
assert not hasattr(group_entity, uncle.upper())


def test_group_entity_with_subroles(
group_entity: entities.GroupEntity, first_parent: str, second_parent: str
) -> None:
"""Assign a Role for each subrole-like passed as argument."""

assert hasattr(group_entity, first_parent.upper())
assert not hasattr(group_entity, second_parent.upper())
11 changes: 11 additions & 0 deletions openfisca_core/entities/tests/test_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from openfisca_core import entities


def test_init_when_doc_indented() -> None:
"""De-indent the ``doc`` attribute if it is passed at initialisation."""

key = "\tkey"
doc = "\tdoc"
role = entities.Role({"key": key, "doc": doc}, object())
assert role.key == key
assert role.doc == doc.lstrip()
6 changes: 1 addition & 5 deletions openfisca_core/entities/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,4 @@


class Entity(Protocol):
"""Entity protocol.
.. versionadded:: 41.0.1
"""
...
Loading

0 comments on commit a3c2f83

Please sign in to comment.