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

test(entities): improve docs, doctests, and typing #1034

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
22d6e63
Add checks to entities
Oct 24, 2021
9e330fa
Make entity a dataclass
Oct 24, 2021
959fc7c
Add docs to Entity
Oct 24, 2021
5a8be59
Add slots to Entity
Oct 24, 2021
77eb6e1
Add typed attributes to Entity
Oct 24, 2021
74e62bb
Refactor __init__ in Entity
Oct 24, 2021
6936801
Fix repr in Entity
Oct 24, 2021
9d3a722
Fix str in Entity
Oct 24, 2021
6a739f1
Make Entity.check_role_validity
Oct 24, 2021
3b70c41
Add HasVariables protocol
Oct 24, 2021
ad51508
Add missing tbs property to Entity
Oct 24, 2021
447dc9b
Deprecate Entity.set_tax_benefit_system
Oct 24, 2021
5c7b4ca
Move Entity,check_role_validity to helpers
Oct 24, 2021
ce9cb6b
Add docs to entities.build_entity
Oct 24, 2021
6bc7246
Add docs to Role
Oct 24, 2021
6b0cdf6
Make SupportsRole runtime-checkable
Oct 24, 2021
d076b39
Add doc to GroupEntity
Oct 24, 2021
4604be5
Extract role building to a function
Oct 24, 2021
a62c4c9
Add pure flatten function to commons
Oct 24, 2021
e698fe4
Add slots to GroupEntity
Oct 24, 2021
92312c5
Fix mutability of group entity
Oct 24, 2021
be3753d
Move role building out of GroupEntity
Oct 24, 2021
5c534ad
Add the variables descriptor
Oct 25, 2021
6acd4a9
Add descriptor to Entity
Oct 25, 2021
14ff209
Add docs to Entity.get_variable
Oct 25, 2021
fa6de71
Add docs to entities.build_role
Oct 25, 2021
eaa7f37
Add exceptions to doctests
Oct 25, 2021
02370cc
Cleanup protocol usage in entities
Oct 25, 2021
e1f06fd
Fix cyclic imports 2/2
Oct 25, 2021
edd3d1a
Rename RoleLike to _RoleSchema
Oct 25, 2021
3f70799
Add missing tests
Oct 26, 2021
590fe5d
Fix generated doc
Oct 26, 2021
1d02114
Cache roles in GroupEntity
Oct 26, 2021
d04d0d1
Fix imports
Oct 26, 2021
f6b67d8
Remove unused commons.first
Oct 26, 2021
03860d9
Remove underused commons.flatten
Oct 26, 2021
845235d
Bump minor to 35.8.0
Oct 25, 2021
30e4fcc
Apply suggestions from code review
Nov 5, 2021
5f4372e
Apply suggestions from code review
Nov 13, 2021
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 35.8.0 [#1034](https://github.com/openfisca/openfisca-core/pull/1034)

#### Documentation

- Complete docs, doctests, and typing of the entities module

### 35.7.1 [#1075](https://github.com/openfisca/openfisca-core/pull/1075)

#### Bug fix
Expand Down
6 changes: 3 additions & 3 deletions openfisca_core/commons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
* :class:`.Dummy`

Note:
The ``deprecated`` imports are transitional, in order to ensure non-breaking
changes, and could be removed from the codebase in the next
The ``deprecated`` imports are transitional, in order to ensure
bonjourmauko marked this conversation as resolved.
Show resolved Hide resolved
non-breaking changes, and could be removed from the codebase in the next
major release.

Note:
Expand All @@ -32,7 +32,7 @@
from modularizing the different components of the library, which would make
them easier to test and to maintain.

How they could be used in a future release:
How they could be used in a future release::
MattiSG marked this conversation as resolved.
Show resolved Hide resolved

from openfisca_core import commons
from openfisca_core.commons import deprecated
Expand Down
4 changes: 3 additions & 1 deletion openfisca_core/commons/formulas.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from __future__ import annotations

from typing import Any, Dict, Sequence, TypeVar
from openfisca_core.typing import ArrayLike, ArrayType

import numpy

from openfisca_core.types import ArrayLike, ArrayType

T = TypeVar("T")

Expand Down
5 changes: 3 additions & 2 deletions openfisca_core/commons/misc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import TypeVar
from __future__ import annotations

from openfisca_core.types import ArrayType
from typing import TypeVar
from openfisca_core.typing import ArrayType

T = TypeVar("T")

Expand Down
5 changes: 3 additions & 2 deletions openfisca_core/commons/rates.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

from typing import Optional
from openfisca_core.typing import ArrayLike, ArrayType

import numpy

from openfisca_core.types import ArrayLike, ArrayType


def average_rate(
target: ArrayType[float],
Expand Down
122 changes: 95 additions & 27 deletions openfisca_core/entities/__init__.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,95 @@
# 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.
bonjourmauko marked this conversation as resolved.
Show resolved Hide resolved
#
# 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

from .helpers import build_entity # noqa: F401
from .role import Role # noqa: F401
from .entity import Entity # noqa: F401
from .group_entity import GroupEntity # noqa: F401
"""Provides a way of representing the entities of a rule system.
bonjourmauko marked this conversation as resolved.
Show resolved Hide resolved

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 indivual, and payroll taxes by a company, as a juridical person. In
OpenFisca, that atomic element is represented as an :class:`.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 :class:`.GroupEntity`.

In the latter case, the atomic members of a given group may have a different
:class:`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 :class:`.Variable`,
is defined either for an :class:`.Entity` or for a :class:`.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 :class:`.Variable` to a
concrete individual or group of individuals, see :class:`.Population`
and :class:`.GroupPopulation`.

Official Public API:
* :class:`.Entity`
* :class:`.GroupEntity`
* :class:`.Role`
* :func:`.build_entity`
* :func:`.check_role_validity`

Deprecated:
* :meth:`.Entity.set_tax_benefit_system`
* :meth:`.Entity.check_role_validity`

Note:
The ``deprecated`` imports are transitional, in order to ensure
non-breaking changes, and could be removed from the codebase in the next
major release.

Note:
How imports are being used today::

from openfisca_core.entities import * # Bad
from openfisca_core.entities.helpers import build_entity # Bad
from openfisca_core.entities.role import Role # Bad

The previous examples provoke cyclic dependency problems, that prevent us
from modularizing the different components of the library, which would make
them easier to test and to maintain.

How they could be used in a future release::

from openfisca_core import entities
from openfisca_core.entities import Role

Role() # Good: import classes as publicly exposed
entities.build_entity() # Good: use functions as publicly exposed

.. seealso:: `PEP8#Imports`_ and `OpenFisca's Styleguide`_.

.. _PEP8#Imports:
https://www.python.org/dev/peps/pep-0008/#imports

.. _OpenFisca's Styleguide:
https://github.com/openfisca/openfisca-core/blob/master/STYLEGUIDE.md

"""

# Official Public API

from .helpers import ( # noqa: F401
Entity,
GroupEntity,
Role,
build_entity,
check_role_validity,
)

__all__ = ["Entity", "GroupEntity", "Role"]
__all__ = ["build_entity", "check_role_validity", *__all__]
174 changes: 174 additions & 0 deletions openfisca_core/entities/_variable_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
from __future__ import annotations

from typing import Any, Optional, Type
from typing_extensions import Protocol
from openfisca_core.typing import (
EntityProtocol,
TaxBenefitSystemProtocol,
VariableProtocol,
)

import functools
import os

doc_url = "https://openfisca.org/doc/coding-the-legislation"


class _Query(Protocol):
"""A dummy class to duck-type :meth:`.TaxBenefitSystem.get_variable`."""

def __call__(
self,
__arg1: str,
__arg2: bool = False,
) -> Optional["_VariableProxy"]:
"""See comment above."""


class _VariableProxy:
"""A `descriptor`_ to find an :obj:`.Entity`'s :obj:`.Variable`.

Attributes:
entity: The :obj:`.Entity` ``owner`` of the descriptor.
tax_benefit_system: The :obj:`.Entity`'s :obj:`.TaxBenefitSystem`.
query: The method used to query the :obj:`.TaxBenefitSystem`.

Examples:
>>> from openfisca_core.entities import Entity
>>> from openfisca_core.taxbenefitsystems import TaxBenefitSystem
>>> from openfisca_core.variables import Variable

>>> entity = Entity(
... "individual",
... "individuals",
... "An individual",
... "The minimal legal entity on which a rule can be applied.",
... )

>>> class Variable(Variable):
... definition_period = "month"
... value_type = float
... entity = entity

>>> tbs = TaxBenefitSystem([entity])
>>> tbs.add_variable(Variable)
<openfisca_core.entities._variable_proxy.Variable...

>>> entity.tax_benefit_system = tbs

>>> entity.variables.get("Variable")
<...Variable...

>>> entity.variables.exists().get("Variable")
<...Variable...

>>> entity.variables.isdefined().get("Variable")
<...Variable...

.. _descriptor: https://docs.python.org/3/howto/descriptor.html

.. versionadded:: 35.8.0

"""

entity: Optional[EntityProtocol] = None
tax_benefit_system: Optional[TaxBenefitSystemProtocol] = None
query: _Query

def __get__(
self,
entity: EntityProtocol,
type: Type[EntityProtocol],
) -> Optional[_VariableProxy]:
"""Binds :meth:`.TaxBenefitSystem.get_variable`."""

self.entity = entity

self.tax_benefit_system = getattr(
self.entity,
"tax_benefit_system",
None,
)

if self.tax_benefit_system is None:
return None

self.query = self.tax_benefit_system.get_variable

return self

def __set__(self, entity: EntityProtocol, value: Any) -> None:
NotImplemented

def get(self, variable_name: str) -> Optional[VariableProtocol]:
"""Runs the query for ``variable_name``, based on the options given.

Args:
variable_name: The :obj:`.Variable` to be found.

Returns:
:obj:`.Variable` or :obj:`None`:
:obj:`.Variable` when the :obj:`.Variable` exists.
:obj:`None` when the :attr:`.tax_benefit_system` is not set.

Raises:
:exc:`.VariableNotFoundError`: When :obj:`.Variable` doesn't exist.
:exc:`.ValueError`: When the :obj:`.Variable` exists but is defined
for another :obj:`.Entity`.

.. versionadded:: 35.8.0

"""

if self.entity is None:
return NotImplemented

return self.query(variable_name)

def exists(self) -> _VariableProxy:
"""Sets ``check_existence`` to ``True``."""

self.query = functools.partial(
self.query,
check_existence = True,
)

return self

def isdefined(self) -> _VariableProxy:
"""Checks that ``variable_name`` is defined for :attr:`.entity`."""

# We assume that we're also checking for existence.
self.exists()

self.query = functools.partial(
self._isdefined,
self.query,
)

return self

def _isdefined(self, query: _Query, variable_name: str, **any: Any) -> Any:
variable = query(variable_name)

if self.entity is None:
return None

if variable is None:
return None

if variable.entity is None:
return None

if self.entity != variable.entity:
message = os.linesep.join([
f"You tried to compute the variable '{variable_name}' for",
f"the entity '{self.entity.plural}'; however the variable",
f"'{variable_name}' is defined for the entity",
f"'{variable.entity.plural}'. Learn more about entities",
f"in our documentation: <{doc_url}/50_entities.html>.",
])

raise ValueError(message)

return variable
Loading