diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b0f3e63a..ade861023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +### 41.5.5 [#1220](https://github.com/openfisca/openfisca-core/pull/1220) + +#### Technical changes + +- Fix doc & type definitions in the entities module + ### 41.5.4 [#1219](https://github.com/openfisca/openfisca-core/pull/1219) #### Technical changes diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index a1cd397a3..9546773cb 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -27,4 +27,14 @@ from .helpers import build_entity, find_role from .role import Role -__all__ = ["Entity", "GroupEntity", "Role", "build_entity", "find_role", "types"] +SingleEntity = Entity + +__all__ = [ + "Entity", + "SingleEntity", + "GroupEntity", + "Role", + "build_entity", + "find_role", + "types", +] diff --git a/openfisca_core/entities/_core_entity.py b/openfisca_core/entities/_core_entity.py index ecd17beca..9a2707d19 100644 --- a/openfisca_core/entities/_core_entity.py +++ b/openfisca_core/entities/_core_entity.py @@ -1,24 +1,19 @@ from __future__ import annotations -from typing import Any - -from openfisca_core.types import TaxBenefitSystem, Variable - import os from abc import abstractmethod +from . import types as t from .role import Role -from .types import Entity class _CoreEntity: """Base class to build entities from.""" #: A key to identify the entity. - key: str - + key: t.EntityKey #: The ``key``, pluralised. - plural: str | None + plural: t.EntityPlural | None #: A summary description. label: str | None @@ -30,16 +25,18 @@ class _CoreEntity: is_person: bool #: A TaxBenefitSystem instance. - _tax_benefit_system: TaxBenefitSystem | None = None + _tax_benefit_system: t.TaxBenefitSystem | None = None @abstractmethod - def __init__(self, key: str, plural: str, label: str, doc: str, *args: Any) -> None: + def __init__( + self, key: str, plural: str, label: str, doc: str, *args: object + ) -> None: ... def __repr__(self) -> str: return f"{self.__class__.__name__}({self.key})" - def set_tax_benefit_system(self, tax_benefit_system: TaxBenefitSystem) -> None: + def set_tax_benefit_system(self, tax_benefit_system: t.TaxBenefitSystem) -> None: """An Entity belongs to a TaxBenefitSystem.""" self._tax_benefit_system = tax_benefit_system @@ -47,14 +44,18 @@ def get_variable( self, variable_name: str, check_existence: bool = False, - ) -> Variable | None: + ) -> t.Variable | None: """Get a ``variable_name`` from ``variables``.""" + if self._tax_benefit_system is None: + raise ValueError( + "You must set 'tax_benefit_system' before calling this method." + ) return self._tax_benefit_system.get_variable(variable_name, check_existence) def check_variable_defined_for_entity(self, variable_name: str) -> None: """Check if ``variable_name`` is defined for ``self``.""" - variable: Variable | None - entity: Entity + variable: t.Variable | None + entity: t.CoreEntity variable = self.get_variable(variable_name, check_existence=True) @@ -71,7 +72,7 @@ def check_variable_defined_for_entity(self, variable_name: str) -> None: ) raise ValueError(os.linesep.join(message)) - def check_role_validity(self, role: Any) -> None: + def check_role_validity(self, role: object) -> None: """Check if a ``role`` is an instance of Role.""" if role is not None and not isinstance(role, Role): raise ValueError(f"{role} is not a valid role") diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index ea2deb505..819477266 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,5 +1,6 @@ import textwrap +from . import types as t from ._core_entity import _CoreEntity @@ -9,8 +10,8 @@ class Entity(_CoreEntity): """ def __init__(self, key: str, plural: str, label: str, doc: str) -> None: - self.key = key + self.key = t.EntityKey(key) self.label = label - self.plural = plural + self.plural = t.EntityPlural(plural) self.doc = textwrap.dedent(doc) self.is_person = True diff --git a/openfisca_core/entities/group_entity.py b/openfisca_core/entities/group_entity.py index eeae52d38..d2242983d 100644 --- a/openfisca_core/entities/group_entity.py +++ b/openfisca_core/entities/group_entity.py @@ -1,11 +1,11 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping -from typing import Any +from collections.abc import Iterable, Sequence import textwrap from itertools import chain +from . import types as t from ._core_entity import _CoreEntity from .role import Role @@ -34,12 +34,12 @@ def __init__( plural: str, label: str, doc: str, - roles: Iterable[Mapping[str, Any]], + roles: Sequence[t.RoleParams], containing_entities: Iterable[str] = (), ) -> None: - self.key = key + self.key = t.EntityKey(key) self.label = label - self.plural = plural + self.plural = t.EntityPlural(plural) self.doc = textwrap.dedent(doc) self.is_person = False self.roles_description = roles diff --git a/openfisca_core/entities/helpers.py b/openfisca_core/entities/helpers.py index 000c8028c..90b9ffd94 100644 --- a/openfisca_core/entities/helpers.py +++ b/openfisca_core/entities/helpers.py @@ -1,22 +1,23 @@ from __future__ import annotations -from typing import Iterable +from collections.abc import Iterable, Sequence +from typing import Optional +from . import types as t from .entity import Entity from .group_entity import GroupEntity -from .types import Role def build_entity( - key, - plural, - label, - doc="", - roles=None, - is_person=False, - class_override=None, - containing_entities=(), -): + key: str, + plural: str, + label: str, + doc: str = "", + roles: Optional[Sequence[t.RoleParams]] = None, + is_person: bool = False, + class_override: object | None = None, + containing_entities: Sequence[str] = (), +) -> t.SingleEntity | t.GroupEntity: """Build a SingleEntity or a GroupEntity. Args: @@ -35,7 +36,7 @@ def build_entity( :obj:`.GroupEntity`: When ``is_person`` is False. Raises: - ValueError if ``roles`` is not a sequence. + NotImplementedError: if ``roles`` is None. Examples: >>> from openfisca_core import entities @@ -73,15 +74,17 @@ def build_entity( if is_person: return Entity(key, plural, label, doc) - else: + if roles is not None: return GroupEntity( key, plural, label, doc, roles, containing_entities=containing_entities ) + raise NotImplementedError + def find_role( - roles: Iterable[Role], key: str, *, total: int | None = None -) -> Role | None: + roles: Iterable[t.Role], key: t.RoleKey, *, total: int | None = None +) -> t.Role | None: """Find a Role in a GroupEntity. Args: diff --git a/openfisca_core/entities/role.py b/openfisca_core/entities/role.py index a4cb75a86..d70357816 100644 --- a/openfisca_core/entities/role.py +++ b/openfisca_core/entities/role.py @@ -6,7 +6,7 @@ import dataclasses import textwrap -from .types import Entity +from .types import SingleEntity class Role: @@ -48,7 +48,7 @@ class Role: """ #: The Entity the Role belongs to. - entity: Entity + entity: SingleEntity #: A description of the Role. description: _Description @@ -79,7 +79,7 @@ def doc(self) -> str | None: """A full description, non-indented.""" return self.description.doc - def __init__(self, description: Mapping[str, Any], entity: Entity) -> None: + def __init__(self, description: Mapping[str, Any], entity: SingleEntity) -> None: self.description = _Description( **{ key: value diff --git a/openfisca_core/entities/types.py b/openfisca_core/entities/types.py index 1a2fb06b2..2f9acd040 100644 --- a/openfisca_core/entities/types.py +++ b/openfisca_core/entities/types.py @@ -1,30 +1,61 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Protocol, TypedDict +from typing import NewType, Protocol +from typing_extensions import Required, TypedDict +from openfisca_core import types as t -class Entity(Protocol): +# Entities + +#: For example "person". +EntityKey = NewType("EntityKey", str) + +#: For example "persons". +EntityPlural = NewType("EntityPlural", str) + +#: For example "principal". +RoleKey = NewType("RoleKey", str) + +#: For example "parents". +RolePlural = NewType("RolePlural", str) + + +class CoreEntity(t.CoreEntity, Protocol): + key: EntityKey + plural: EntityPlural | None + + +class SingleEntity(t.SingleEntity, Protocol): ... -class GroupEntity(Protocol): +class GroupEntity(t.GroupEntity, Protocol): ... -class Role(Protocol): - max: int | None +class Role(t.Role, Protocol): subroles: Iterable[Role] | None - @property - def key(self) -> str: - ... - class RoleParams(TypedDict, total=False): - key: str + key: Required[str] plural: str label: str doc: str max: int subroles: list[str] + + +# Tax-Benefit systems + + +class TaxBenefitSystem(t.TaxBenefitSystem, Protocol): + ... + + +# Variables + + +class Variable(t.Variable, Protocol): + ... diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index 06d680388..e3ef6b209 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -3,7 +3,7 @@ from typing import Dict, NamedTuple, Optional, Sequence, Union from typing_extensions import TypedDict -from openfisca_core.types import Array, Entity, Period, Role, Simulation +from openfisca_core.types import Array, Period, Role, Simulation, SingleEntity import traceback @@ -16,12 +16,12 @@ class Population: simulation: Optional[Simulation] - entity: Entity + entity: SingleEntity _holders: Dict[str, holders.Holder] count: int ids: Array[str] - def __init__(self, entity: Entity) -> None: + def __init__(self, entity: SingleEntity) -> None: self.simulation = None self.entity = entity self._holders = {} diff --git a/openfisca_core/projectors/helpers.py b/openfisca_core/projectors/helpers.py index ce94f9773..b3b7e6f2d 100644 --- a/openfisca_core/projectors/helpers.py +++ b/openfisca_core/projectors/helpers.py @@ -2,7 +2,7 @@ from collections.abc import Mapping -from openfisca_core.entities.types import Entity, GroupEntity, Role +from openfisca_core.types import GroupEntity, Role, SingleEntity from openfisca_core import entities, projectors @@ -110,7 +110,7 @@ def get_projector_from_shortcut( """ - entity: Entity | GroupEntity = population.entity + entity: SingleEntity | GroupEntity = population.entity if isinstance(entity, entities.Entity): populations: Mapping[ diff --git a/openfisca_core/projectors/typing.py b/openfisca_core/projectors/typing.py index a6ce8e398..186f90e30 100644 --- a/openfisca_core/projectors/typing.py +++ b/openfisca_core/projectors/typing.py @@ -3,12 +3,12 @@ from collections.abc import Mapping from typing import Protocol -from openfisca_core.entities.types import Entity, GroupEntity +from openfisca_core.types import GroupEntity, SingleEntity class Population(Protocol): @property - def entity(self) -> Entity: + def entity(self) -> SingleEntity: ... @property diff --git a/openfisca_core/types.py b/openfisca_core/types.py index ede4ba9bc..b34a55543 100644 --- a/openfisca_core/types.py +++ b/openfisca_core/types.py @@ -30,7 +30,7 @@ # Entities -class Entity(Protocol): +class CoreEntity(Protocol): key: Any plural: Any @@ -51,10 +51,23 @@ def get_variable( ... +class SingleEntity(CoreEntity, Protocol): + ... + + +class GroupEntity(CoreEntity, Protocol): + ... + + class Role(Protocol): entity: Any + max: int | None subroles: Any + @property + def key(self) -> str: + ... + # Holders diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 828c51800..445abba10 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -41,6 +41,7 @@ check-types: @$(call print_help,$@:) @mypy \ openfisca_core/commons \ + openfisca_core/entities \ openfisca_core/types.py @$(call print_pass,$@:) diff --git a/setup.cfg b/setup.cfg index e266be625..cc850c06a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,85 +1,86 @@ -# C011X: We (progressively) document the code base. -# D10X: We (progressively) check docstrings (see https://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping). -# DARXXX: We (progressively) check docstrings (see https://github.com/terrencepreilly/darglint#error-codes). -# E203: We ignore a false positive in whitespace before ":" (see https://github.com/PyCQA/pycodestyle/issues/373). -# F403/405: We ignore * imports. -# R0401: We avoid cyclic imports —required for unit/doc tests. -# RST301: We use Google Python Style (see https://pypi.org/project/flake8-rst-docstrings/). -# W503/504: We break lines before binary operators (Knuth's style). +# C011X: We (progressively) document the code base. +# D10X: We (progressively) check docstrings (see https://www.pydocstyle.org/en/2.1.1/error_codes.html#grouping). +# DARXXX: We (progressively) check docstrings (see https://github.com/terrencepreilly/darglint#error-codes). +# E203: We ignore a false positive in whitespace before ":" (see https://github.com/PyCQA/pycodestyle/issues/373). +# F403/405: We ignore * imports. +# R0401: We avoid cyclic imports —required for unit/doc tests. +# RST301: We use Google Python Style (see https://pypi.org/project/flake8-rst-docstrings/). +# W503/504: We break lines before binary operators (Knuth's style). [flake8] -convention = google -docstring_style = google -extend-ignore = D -ignore = E203, E501, F405, RST301, W503 -in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors -max-line-length = 88 -per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 -rst-directives = attribute, deprecated, seealso, versionadded, versionchanged -rst-roles = any, attr, class, exc, func, meth, mod, obj -strictness = short +convention = google +docstring_style = google +extend-ignore = D +ignore = E203, E501, F405, RST301, W503 +in-place = true +include-in-doctest = openfisca_core/commons openfisca_core/entities openfisca_core/holders openfisca_core/periods openfisca_core/projectors +max-line-length = 88 +per-file-ignores = */types.py:D101,D102,E704, */test_*.py:D101,D102,D103, */__init__.py:F401 +rst-directives = attribute, deprecated, seealso, versionadded, versionchanged +rst-roles = any, attr, class, exc, func, meth, mod, obj +strictness = short [pylint.MASTER] -load-plugins = pylint_per_file_ignores +load-plugins = pylint_per_file_ignores [pylint.message_control] -disable = all -enable = C0115, C0116, R0401 -per-file-ignores = +disable = all +enable = C0115, C0116, R0401 +per-file-ignores = types.py:C0115,C0116 /tests/:C0116 -score = no +score = no [isort] -case_sensitive = true +case_sensitive = true force_alphabetical_sort_within_sections = false -group_by_package = true -honor_noqa = true -include_trailing_comma = true -known_first_party = openfisca_core -known_openfisca = openfisca_country_template, openfisca_extension_template -known_typing = *collections.abc*, *typing*, *typing_extensions* -known_types = *types* -profile = black -py_version = 39 -sections = FUTURE, TYPING, TYPES, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER +group_by_package = true +honor_noqa = true +include_trailing_comma = true +known_first_party = openfisca_core +known_openfisca = openfisca_country_template, openfisca_extension_template +known_typing = *collections.abc*, *typing*, *typing_extensions* +known_types = *types* +profile = black +py_version = 39 +sections = FUTURE, TYPING, TYPES, STDLIB, THIRDPARTY, OPENFISCA, FIRSTPARTY, LOCALFOLDER [coverage:paths] -source = . */site-packages +source = . */site-packages [coverage:run] -branch = true -source = openfisca_core, openfisca_web_api +branch = true +source = openfisca_core, openfisca_web_api [coverage:report] -fail_under = 75 -show_missing = true -skip_covered = true -skip_empty = true +fail_under = 75 +show_missing = true +skip_covered = true +skip_empty = true [tool:pytest] -addopts = --doctest-modules --disable-pytest-warnings --showlocals -doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE -python_files = **/*.py -testpaths = tests +addopts = --disable-pytest-warnings --doctest-modules --showlocals +doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE +python_files = **/*.py +testpaths = tests [mypy] -disallow_any_unimported = true -ignore_missing_imports = true -install_types = true -non_interactive = true -plugins = numpy.typing.mypy_plugin -python_version = 3.9 +check_untyped_defs = false +disallow_any_decorated = false +disallow_any_explicit = false +disallow_any_expr = false +disallow_any_unimported = false +follow_imports = skip +ignore_missing_imports = true +implicit_reexport = false +install_types = true +non_interactive = true +plugins = numpy.typing.mypy_plugin +pretty = true +python_version = 3.9 +strict = false +warn_no_return = true +warn_unreachable = true -[mypy-openfisca_core.commons.tests.*] -ignore_errors = True - -[mypy-openfisca_core.holders.tests.*] -ignore_errors = True - -[mypy-openfisca_core.periods.tests.*] -ignore_errors = True - -[mypy-openfisca_core.scripts.*] -ignore_errors = True +[mypy-openfisca_core.*.tests.*] +ignore_errors = True diff --git a/setup.py b/setup.py index 92971f40b..cca107bee 100644 --- a/setup.py +++ b/setup.py @@ -67,7 +67,7 @@ setup( name="OpenFisca-Core", - version="41.5.4", + version="41.5.5", author="OpenFisca Team", author_email="contact@openfisca.org", classifiers=[