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

[3/17] Add tests to entities #1192

Merged
merged 13 commits into from
Oct 3, 2023
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
Loading