diff --git a/CHANGELOG.md b/CHANGELOG.md index 7493c4e25..9f3d7d278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index a08198704..c90b9d0d6 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -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"] diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index b0f9e2a0d..2501cec80 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -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 @@ -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, @@ -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:", - ".", - ] + 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:", + ".", ) - raise ValueError(message) + raise ValueError(os.linesep.join(message)) diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index f6472d2c5..0c787da5b 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -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 @@ -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 = [] @@ -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 diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 6e0ab55ac..43969fcd7 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,4 +1,5 @@ -from openfisca_core import entities +from .entity import Entity +from .group_entity import GroupEntity def build_entity( @@ -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 ) diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index 5577da365..976711b60 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Mapping, Sequence from typing import Any import dataclasses @@ -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. @@ -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) - "" + >>> repr(_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 @@ -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)) diff --git a/openfisca_core/entities/tests/__init__.py b/openfisca_core/entities/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/openfisca_core/entities/tests/test_entity.py b/openfisca_core/entities/tests/test_entity.py new file mode 100644 index 000000000..488d271ff --- /dev/null +++ b/openfisca_core/entities/tests/test_entity.py @@ -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() 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 000000000..ed55648d7 --- /dev/null +++ b/openfisca_core/entities/tests/test_group_entity.py @@ -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()) diff --git a/openfisca_core/entities/tests/test_role.py b/openfisca_core/entities/tests/test_role.py new file mode 100644 index 000000000..83692e823 --- /dev/null +++ b/openfisca_core/entities/tests/test_role.py @@ -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() diff --git a/openfisca_core/entities/typing.py b/openfisca_core/entities/typing.py index ab2640d21..5a1e60d4a 100644 --- a/openfisca_core/entities/typing.py +++ b/openfisca_core/entities/typing.py @@ -2,8 +2,4 @@ class Entity(Protocol): - """Entity protocol. - - .. versionadded:: 41.0.1 - - """ + ... diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 69a1a5095..7b958dd1e 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -36,23 +36,7 @@ lint-doc-%: ## Run static type checkers for type errors. check-types: @$(call print_help,$@:) - @mypy --package openfisca_core --package openfisca_web_api - @$(call print_pass,$@:) - -## Run static type checkers for type errors (strict). -lint-typing-strict: \ - lint-typing-strict-commons \ - lint-typing-strict-types \ - ; - -## Run static type checkers for type errors (strict). -lint-typing-strict-%: - @$(call print_help,$(subst $*,%,$@:)) - @mypy \ - --cache-dir .mypy_cache-openfisca_core.$* \ - --implicit-reexport \ - --strict \ - --package openfisca_core.$* + @mypy openfisca_core/entities @$(call print_pass,$@:) ## Run code formatters to correct style errors. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..1e2a43ee4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +target-version = ["py39", "py310", "py311"] diff --git a/setup.py b/setup.py index 99a3c00e9..07fd5c2c9 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.1.1", + version="41.1.2", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[