diff --git a/.github/get-numpy-version.py b/.github/get-numpy-version.py index 64cb68532e..3120357b27 100755 --- a/.github/get-numpy-version.py +++ b/.github/get-numpy-version.py @@ -2,13 +2,14 @@ from __future__ import annotations -import os -import sys import typing -from packaging import version from typing import NoReturn, Union +import os +import sys + import numpy +from packaging import version if typing.TYPE_CHECKING: from packaging.version import LegacyVersion, Version diff --git a/CHANGELOG.md b/CHANGELOG.md index 601cdab211..32dcefc876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,42 @@ # Changelog +## 39.1.0 [#1139](https://github.com/openfisca/openfisca-core/pull/1139) + +#### New features + +- Introduce `DateUnit` to `periods` + - Allows for transparent encapsulation of the atomic units of `periods` (`year`, `month`, ...). + - This helps with testing, refactoring and exrtension of `periods` (`weeks` for example). + + +# 39.0.0 [#1138](https://github.com/openfisca/openfisca-core/pull/1138) + +#### Breaking changes + +- Deprecate `instant_date`. +- Deprecate `periods.intersect`. +- Deprecate `periods.unit_weight`. +- Deprecate `periods.unit_weights`. +- Make `periods.parse_period` stricter (for example `2022-1` now fails). +- Refactor `Period.contains` as `Period.__contains__`. +- Rename `Period.get_subperiods` to `subperiods`. +- Rename `instant` to `build_instant`. +- Rename `period` to `build_period`. +- Transform `Instant.date` from property to method. +- Transform `Period.date` from property to method. + +#### Technical changes + +- Add typing to `openfisca_core.periods`. +- Document `openfisca_core.periods`. +- Fix `openfisca_core.periods` doctests. + +#### Bug fixes + +- Fixes impossible `last-of` and `first-of` offsets. +- Fixes incoherent dates, +- Fixes several race conditions, + # 38.0.0 [#989](https://github.com/openfisca/openfisca-core/pull/989) #### New Features diff --git a/openfisca_core/commons/formulas.py b/openfisca_core/commons/formulas.py index 4e13b925fb..e7c8067504 100644 --- a/openfisca_core/commons/formulas.py +++ b/openfisca_core/commons/formulas.py @@ -1,9 +1,8 @@ +from openfisca_core.types import Array, ArrayLike from typing import Any, Dict, Sequence, TypeVar import numpy -from openfisca_core.types import ArrayLike, Array - T = TypeVar("T") diff --git a/openfisca_core/commons/misc.py b/openfisca_core/commons/misc.py index c2edcb25dc..c5aba12cdc 100644 --- a/openfisca_core/commons/misc.py +++ b/openfisca_core/commons/misc.py @@ -1,6 +1,5 @@ -from typing import TypeVar - from openfisca_core.types import Array +from typing import TypeVar T = TypeVar("T") diff --git a/openfisca_core/commons/rates.py b/openfisca_core/commons/rates.py index e9d67c322a..15c6471aff 100644 --- a/openfisca_core/commons/rates.py +++ b/openfisca_core/commons/rates.py @@ -1,9 +1,8 @@ +from openfisca_core.types import Array, ArrayLike from typing import Optional import numpy -from openfisca_core.types import ArrayLike, Array - def average_rate( target: Array[float], diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py index bd40460a56..123472c638 100644 --- a/openfisca_core/data_storage/in_memory_storage.py +++ b/openfisca_core/data_storage/in_memory_storage.py @@ -14,8 +14,8 @@ def __init__(self, is_eternal = False): def get(self, period): if self.is_eternal: - period = periods.period(periods.ETERNITY) - period = periods.period(period) + period = periods.build_period(periods.DateUnit.ETERNITY) + period = periods.build_period(period) values = self._arrays.get(period) if values is None: @@ -24,8 +24,8 @@ def get(self, period): def put(self, value, period): if self.is_eternal: - period = periods.period(periods.ETERNITY) - period = periods.period(period) + period = periods.build_period(periods.DateUnit.ETERNITY) + period = periods.build_period(period) self._arrays[period] = value @@ -35,13 +35,13 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.period(periods.ETERNITY) - period = periods.period(period) + period = periods.build_period(periods.DateUnit.ETERNITY) + period = periods.build_period(period) self._arrays = { period_item: value for period_item, value in self._arrays.items() - if not period.contains(period_item) + if period_item not in period } def get_known_periods(self): diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py index 10d4696b58..9aa79ab98a 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -5,6 +5,7 @@ from openfisca_core import periods from openfisca_core.indexed_enums import EnumArray +from openfisca_core.periods import DateUnit class OnDiskStorage: @@ -28,8 +29,8 @@ def _decode_file(self, file): def get(self, period): if self.is_eternal: - period = periods.period(periods.ETERNITY) - period = periods.period(period) + period = periods.build_period(DateUnit.ETERNITY) + period = periods.build_period(period) values = self._files.get(period) if values is None: @@ -38,8 +39,8 @@ def get(self, period): def put(self, value, period): if self.is_eternal: - period = periods.period(periods.ETERNITY) - period = periods.period(period) + period = periods.build_period(DateUnit.ETERNITY) + period = periods.build_period(period) filename = str(period) path = os.path.join(self.storage_dir, filename) + '.npy' @@ -55,14 +56,14 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.period(periods.ETERNITY) - period = periods.period(period) + period = periods.build_period(DateUnit.ETERNITY) + period = periods.build_period(period) if period is not None: self._files = { period_item: value for period_item, value in self._files.items() - if not period.contains(period_item) + if period_item not in period } def get_known_periods(self): @@ -76,7 +77,7 @@ def restore(self): continue path = os.path.join(self.storage_dir, filename) filename_core = filename.rsplit('.', 1)[0] - period = periods.period(filename_core) + period = periods.build_period(filename_core) files[period] = path def __del__(self): diff --git a/openfisca_core/entities/__init__.py b/openfisca_core/entities/__init__.py index 15b38e2a5c..d300cd4ba8 100644 --- a/openfisca_core/entities/__init__.py +++ b/openfisca_core/entities/__init__.py @@ -21,7 +21,7 @@ # # 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 +from .helpers import build_entity # noqa: F401 diff --git a/openfisca_core/entities/entity.py b/openfisca_core/entities/entity.py index 1be0e89fa1..642e072cda 100644 --- a/openfisca_core/entities/entity.py +++ b/openfisca_core/entities/entity.py @@ -1,9 +1,9 @@ +from openfisca_core.types import TaxBenefitSystem, Variable from typing import Any, Optional import os import textwrap -from openfisca_core.types import TaxBenefitSystem, Variable from openfisca_core.entities import Role diff --git a/openfisca_core/errors/__init__.py b/openfisca_core/errors/__init__.py index ccd19af9b2..9973e12c22 100644 --- a/openfisca_core/errors/__init__.py +++ b/openfisca_core/errors/__init__.py @@ -24,10 +24,13 @@ from .cycle_error import CycleError # noqa: F401 from .empty_argument_error import EmptyArgumentError # noqa: F401 from .nan_creation_error import NaNCreationError # noqa: F401 -from .parameter_not_found_error import ParameterNotFoundError, ParameterNotFoundError as ParameterNotFound # noqa: F401 +from .parameter_not_found_error import ParameterNotFoundError # noqa: F401 +from .parameter_not_found_error import ParameterNotFoundError as ParameterNotFound from .parameter_parsing_error import ParameterParsingError # noqa: F401 from .period_mismatch_error import PeriodMismatchError # noqa: F401 from .situation_parsing_error import SituationParsingError # noqa: F401 from .spiral_error import SpiralError # noqa: F401 -from .variable_name_config_error import VariableNameConflictError, VariableNameConflictError as VariableNameConflict # noqa: F401 -from .variable_not_found_error import VariableNotFoundError, VariableNotFoundError as VariableNotFound # noqa: F401 +from .variable_name_config_error import VariableNameConflictError # noqa: F401 +from .variable_name_config_error import VariableNameConflictError as VariableNameConflict +from .variable_not_found_error import VariableNotFoundError # noqa: F401 +from .variable_not_found_error import VariableNotFoundError as VariableNotFound diff --git a/openfisca_core/errors/empty_argument_error.py b/openfisca_core/errors/empty_argument_error.py index d3bcddbf9a..697be070ed 100644 --- a/openfisca_core/errors/empty_argument_error.py +++ b/openfisca_core/errors/empty_argument_error.py @@ -1,6 +1,7 @@ +import typing + import os import traceback -import typing import numpy diff --git a/openfisca_core/formula_helpers.py b/openfisca_core/formula_helpers.py index e0c755348e..96e7a1e8d8 100644 --- a/openfisca_core/formula_helpers.py +++ b/openfisca_core/formula_helpers.py @@ -6,4 +6,8 @@ # The following are transitional imports to ensure non-breaking changes. # Could be deprecated in the next major release. -from openfisca_core.commons import apply_thresholds, concat, switch # noqa: F401 +from openfisca_core.commons import ( # noqa: F401 + apply_thresholds, + concat, + switch, + ) diff --git a/openfisca_core/holders/__init__.py b/openfisca_core/holders/__init__.py index 8bd7722ba3..bcd55dc0a7 100644 --- a/openfisca_core/holders/__init__.py +++ b/openfisca_core/holders/__init__.py @@ -21,6 +21,9 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .helpers import set_input_dispatch_by_period, set_input_divide_by_period # noqa: F401 +from .helpers import ( # noqa: F401 + set_input_dispatch_by_period, + set_input_divide_by_period, + ) from .holder import Holder # noqa: F401 from .memory_usage import MemoryUsage # noqa: F401 diff --git a/openfisca_core/holders/helpers.py b/openfisca_core/holders/helpers.py index 176f6b6f30..dcabd19548 100644 --- a/openfisca_core/holders/helpers.py +++ b/openfisca_core/holders/helpers.py @@ -21,7 +21,7 @@ def set_input_dispatch_by_period(holder, period, array): period_size = period.size period_unit = period.unit - if holder.variable.definition_period == periods.ETERNITY: + if holder.variable.definition_period == periods.DateUnit.ETERNITY: raise ValueError("set_input_dispatch_by_period can't be used for eternal variables.") cached_period_unit = holder.variable.definition_period @@ -53,7 +53,7 @@ def set_input_divide_by_period(holder, period, array): period_size = period.size period_unit = period.unit - if holder.variable.definition_period == periods.ETERNITY: + if holder.variable.definition_period == periods.DateUnit.ETERNITY: raise ValueError("set_input_divide_by_period can't be used for eternal variables.") cached_period_unit = holder.variable.definition_period diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index ae7e3fbcec..b64f156617 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -1,5 +1,6 @@ from __future__ import annotations +from openfisca_core.periods.typing import Period from typing import Any, Optional, Sequence, Union import os @@ -8,15 +9,11 @@ import numpy import psutil -from openfisca_core import ( - errors, - commons, - data_storage as storage, - indexed_enums as enums, - periods, - tools, - types, - ) +from openfisca_core import commons +from openfisca_core import data_storage as storage +from openfisca_core import errors +from openfisca_core import indexed_enums as enums +from openfisca_core import periods, tools from .memory_usage import MemoryUsage @@ -30,7 +27,7 @@ def __init__(self, variable, population): self.population = population self.variable = variable self.simulation = population.simulation - self._memory_storage = storage.InMemoryStorage(is_eternal = (self.variable.definition_period == periods.ETERNITY)) + self._memory_storage = storage.InMemoryStorage(is_eternal = (self.variable.definition_period == periods.DateUnit.ETERNITY)) # By default, do not activate on-disk storage, or variable dropping self._disk_storage = None @@ -67,7 +64,7 @@ def create_disk_storage(self, directory = None, preserve = False): os.mkdir(storage_dir) return storage.OnDiskStorage( storage_dir, - is_eternal = (self.variable.definition_period == periods.ETERNITY), + is_eternal = (self.variable.definition_period == periods.DateUnit.ETERNITY), preserve_storage_dir = preserve ) @@ -164,7 +161,7 @@ def get_known_periods(self): def set_input( self, - period: types.Period, + period: Period, array: Union[numpy.ndarray, Sequence[Any]], ) -> Optional[numpy.ndarray]: """Set a Variable's array of values of a given Period. @@ -210,10 +207,14 @@ def set_input( """ - period = periods.period(period) - if period.unit == periods.ETERNITY and self.variable.definition_period != periods.ETERNITY: + period = periods.build_period(period) + + if period is None: + raise ValueError(f"Invalid period value: {period}") + + if period.unit == periods.DateUnit.ETERNITY and self.variable.definition_period != periods.DateUnit.ETERNITY: error_message = os.linesep.join([ - 'Unable to set a value for variable {0} for periods.ETERNITY.', + 'Unable to set a value for variable {0} for DateUnit.ETERNITY.', '{0} is only defined for {1}s. Please adapt your input.', ]).format( self.variable.name, @@ -260,9 +261,9 @@ def _to_array(self, value): def _set(self, period, value): value = self._to_array(value) - if self.variable.definition_period != periods.ETERNITY: + if self.variable.definition_period != periods.DateUnit.ETERNITY: if period is None: - raise ValueError('A period must be specified to set values, except for variables with periods.ETERNITY as as period_definition.') + raise ValueError('A period must be specified to set values, except for variables with DateUnit.ETERNITY as as period_definition.') if (self.variable.definition_period != period.unit or period.size > 1): name = self.variable.name period_size_adj = f'{period.unit}' if (period.size == 1) else f'{period.size}-{period.unit}s' diff --git a/openfisca_core/holders/tests/test_helpers.py b/openfisca_core/holders/tests/test_helpers.py index 6ba1e7a815..0a6dd885c9 100644 --- a/openfisca_core/holders/tests/test_helpers.py +++ b/openfisca_core/holders/tests/test_helpers.py @@ -37,15 +37,15 @@ def population(people): @pytest.mark.parametrize("dispatch_unit, definition_unit, values, expected", [ - [periods.YEAR, periods.YEAR, [1.], [3.]], - [periods.YEAR, periods.MONTH, [1.], [36.]], - [periods.YEAR, periods.DAY, [1.], [1096.]], - [periods.MONTH, periods.YEAR, [1.], [1.]], - [periods.MONTH, periods.MONTH, [1.], [3.]], - [periods.MONTH, periods.DAY, [1.], [90.]], - [periods.DAY, periods.YEAR, [1.], [1.]], - [periods.DAY, periods.MONTH, [1.], [1.]], - [periods.DAY, periods.DAY, [1.], [3.]], + [periods.DateUnit.YEAR, periods.DateUnit.YEAR, [1.], [3.]], + [periods.DateUnit.YEAR, periods.DateUnit.MONTH, [1.], [36.]], + [periods.DateUnit.YEAR, periods.DateUnit.DAY, [1.], [1096.]], + [periods.DateUnit.MONTH, periods.DateUnit.YEAR, [1.], [1.]], + [periods.DateUnit.MONTH, periods.DateUnit.MONTH, [1.], [3.]], + [periods.DateUnit.MONTH, periods.DateUnit.DAY, [1.], [90.]], + [periods.DateUnit.DAY, periods.DateUnit.YEAR, [1.], [1.]], + [periods.DateUnit.DAY, periods.DateUnit.MONTH, [1.], [1.]], + [periods.DateUnit.DAY, periods.DateUnit.DAY, [1.], [3.]], ]) def test_set_input_dispatch_by_period( Income, @@ -68,15 +68,15 @@ def test_set_input_dispatch_by_period( @pytest.mark.parametrize("divide_unit, definition_unit, values, expected", [ - [periods.YEAR, periods.YEAR, [3.], [1.]], - [periods.YEAR, periods.MONTH, [36.], [1.]], - [periods.YEAR, periods.DAY, [1095.], [1.]], - [periods.MONTH, periods.YEAR, [1.], [1.]], - [periods.MONTH, periods.MONTH, [3.], [1.]], - [periods.MONTH, periods.DAY, [90.], [1.]], - [periods.DAY, periods.YEAR, [1.], [1.]], - [periods.DAY, periods.MONTH, [1.], [1.]], - [periods.DAY, periods.DAY, [3.], [1.]], + [periods.DateUnit.YEAR, periods.DateUnit.YEAR, [3.], [1.]], + [periods.DateUnit.YEAR, periods.DateUnit.MONTH, [36.], [1.]], + [periods.DateUnit.YEAR, periods.DateUnit.DAY, [1095.], [1.]], + [periods.DateUnit.MONTH, periods.DateUnit.YEAR, [1.], [1.]], + [periods.DateUnit.MONTH, periods.DateUnit.MONTH, [3.], [1.]], + [periods.DateUnit.MONTH, periods.DateUnit.DAY, [90.], [1.]], + [periods.DateUnit.DAY, periods.DateUnit.YEAR, [1.], [1.]], + [periods.DateUnit.DAY, periods.DateUnit.MONTH, [1.], [1.]], + [periods.DateUnit.DAY, periods.DateUnit.DAY, [3.], [1.]], ]) def test_set_input_divide_by_period( Income, diff --git a/openfisca_core/indexed_enums/enum.py b/openfisca_core/indexed_enums/enum.py index 3d9fc08447..edd70cb24e 100644 --- a/openfisca_core/indexed_enums/enum.py +++ b/openfisca_core/indexed_enums/enum.py @@ -1,8 +1,9 @@ from __future__ import annotations -import enum from typing import Union +import enum + import numpy from . import ENUM_ARRAY_DTYPE, EnumArray diff --git a/openfisca_core/model_api.py b/openfisca_core/model_api.py index 8ccf5c2763..a3b076893e 100644 --- a/openfisca_core/model_api.py +++ b/openfisca_core/model_api.py @@ -1,39 +1,34 @@ from datetime import date # noqa: F401 -from numpy import ( # noqa: F401 - logical_not as not_, - maximum as max_, - minimum as min_, - round as round_, - select, - where, +from numpy import logical_not as not_ # noqa: F401 +from numpy import maximum as max_ # noqa: F401 +from numpy import minimum as min_ # noqa: F401 +from numpy import round as round_ # noqa: F401 +from numpy import select, where # noqa: F401 + +from openfisca_core.commons import ( # noqa: F401 + apply_thresholds, + concat, + switch, ) - -from openfisca_core.commons import apply_thresholds, concat, switch # noqa: F401 - from openfisca_core.holders import ( # noqa: F401 set_input_dispatch_by_period, set_input_divide_by_period, ) - from openfisca_core.indexed_enums import Enum # noqa: F401 - from openfisca_core.parameters import ( # noqa: F401 + Bracket, load_parameter_file, + Parameter, ParameterNode, Scale, - Bracket, - Parameter, ValuesHistory, ) - -from openfisca_core.periods import DAY, MONTH, YEAR, ETERNITY, period # noqa: F401 +from openfisca_core.periods import build_period, DateUnit # noqa: F401 from openfisca_core.populations import ADD, DIVIDE # noqa: F401 from openfisca_core.reforms import Reform # noqa: F401 - from openfisca_core.simulations import ( # noqa: F401 calculate_output_add, calculate_output_divide, ) - from openfisca_core.variables import Variable # noqa: F401 diff --git a/openfisca_core/parameters/__init__.py b/openfisca_core/parameters/__init__.py index 040ae47056..a5dfa7ea6a 100644 --- a/openfisca_core/parameters/__init__.py +++ b/openfisca_core/parameters/__init__.py @@ -21,24 +21,27 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from openfisca_core.errors import ParameterNotFound, ParameterParsingError # noqa: F401 - +from openfisca_core.errors import ( # noqa: F401 + ParameterNotFound, + ParameterParsingError, + ) +from .at_instant_like import AtInstantLike # noqa: F401 from .config import ( # noqa: F401 ALLOWED_PARAM_TYPES, COMMON_KEYS, - FILE_EXTENSIONS, date_constructor, dict_no_duplicate_constructor, + FILE_EXTENSIONS, ) - -from .at_instant_like import AtInstantLike # noqa: F401 from .helpers import contains_nan, load_parameter_file # noqa: F401 from .parameter_at_instant import ParameterAtInstant # noqa: F401 -from .parameter_node_at_instant import ParameterNodeAtInstant # noqa: F401 -from .vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant # noqa: F401 from .parameter import Parameter # noqa: F401 +from .parameter_node_at_instant import ParameterNodeAtInstant # noqa: F401 from .parameter_node import ParameterNode # noqa: F401 -from .parameter_scale import ParameterScale, ParameterScale as Scale # noqa: F401 -from .parameter_scale_bracket import ParameterScaleBracket, ParameterScaleBracket as Bracket # noqa: F401 +from .parameter_scale import ParameterScale # noqa: F401 +from .parameter_scale import ParameterScale as Scale +from .parameter_scale_bracket import ParameterScaleBracket # noqa: F401 +from .parameter_scale_bracket import ParameterScaleBracket as Bracket from .values_history import ValuesHistory # noqa: F401 +from .vectorial_parameter_node_at_instant import VectorialParameterNodeAtInstant # noqa: F401 diff --git a/openfisca_core/parameters/at_instant_like.py b/openfisca_core/parameters/at_instant_like.py index 1a1db34beb..1b799b24ed 100644 --- a/openfisca_core/parameters/at_instant_like.py +++ b/openfisca_core/parameters/at_instant_like.py @@ -12,7 +12,7 @@ def __call__(self, instant): return self.get_at_instant(instant) def get_at_instant(self, instant): - instant = str(periods.instant(instant)) + instant = str(periods.build_instant(instant)) return self._get_at_instant(instant) @abc.abstractmethod diff --git a/openfisca_core/parameters/config.py b/openfisca_core/parameters/config.py index e9a3041ae8..d037c768ce 100644 --- a/openfisca_core/parameters/config.py +++ b/openfisca_core/parameters/config.py @@ -1,9 +1,11 @@ -import warnings +import typing + import os +import warnings + import yaml -import typing -from openfisca_core.warnings import LibYAMLWarning +from openfisca_core.warnings import LibYAMLWarning try: from yaml import CLoader as Loader diff --git a/openfisca_core/parameters/parameter.py b/openfisca_core/parameters/parameter.py index ed2c9482a4..006c1e3c5c 100644 --- a/openfisca_core/parameters/parameter.py +++ b/openfisca_core/parameters/parameter.py @@ -7,7 +7,12 @@ from openfisca_core import commons, periods from openfisca_core.errors import ParameterParsingError -from openfisca_core.parameters import config, helpers, AtInstantLike, ParameterAtInstant +from openfisca_core.parameters import ( + AtInstantLike, + config, + helpers, + ParameterAtInstant, + ) class Parameter(AtInstantLike): @@ -120,7 +125,7 @@ def update(self, period = None, start = None, stop = None, value = None): if start is not None or stop is not None: raise TypeError("Wrong input for 'update' method: use either 'update(period, value = value)' or 'update(start = start, stop = stop, value = value)'. You cannot both use 'period' and 'start' or 'stop'.") if isinstance(period, str): - period = periods.period(period) + period = periods.build_period(period) start = period.start stop = period.stop if start is None: diff --git a/openfisca_core/parameters/parameter_at_instant.py b/openfisca_core/parameters/parameter_at_instant.py index a383258346..2c5ca5453c 100644 --- a/openfisca_core/parameters/parameter_at_instant.py +++ b/openfisca_core/parameters/parameter_at_instant.py @@ -1,6 +1,7 @@ -import copy import typing +import copy + from openfisca_core import commons from openfisca_core.errors import ParameterParsingError from openfisca_core.parameters import config, helpers diff --git a/openfisca_core/parameters/parameter_node.py b/openfisca_core/parameters/parameter_node.py index 6b636cd9ca..609fac87cc 100644 --- a/openfisca_core/parameters/parameter_node.py +++ b/openfisca_core/parameters/parameter_node.py @@ -1,11 +1,13 @@ from __future__ import annotations +import typing + import copy import os -import typing from openfisca_core import commons, parameters, tools -from . import config, helpers, AtInstantLike, Parameter, ParameterNodeAtInstant + +from . import AtInstantLike, config, helpers, Parameter, ParameterNodeAtInstant class ParameterNode(AtInstantLike): diff --git a/openfisca_core/parameters/parameter_scale.py b/openfisca_core/parameters/parameter_scale.py index 0f7b350732..85389d006b 100644 --- a/openfisca_core/parameters/parameter_scale.py +++ b/openfisca_core/parameters/parameter_scale.py @@ -1,10 +1,11 @@ +import typing + import copy import os -import typing from openfisca_core import commons, parameters, tools from openfisca_core.errors import ParameterParsingError -from openfisca_core.parameters import config, helpers, AtInstantLike +from openfisca_core.parameters import AtInstantLike, config, helpers from openfisca_core.taxscales import ( LinearAverageRateTaxScale, MarginalAmountTaxScale, diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 8acddd62c9..ef0fcc3988 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -1,45 +1,36 @@ -# 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. -# -# How could them be used after the next major release: -# -# >>> from openfisca_core import module -# >>> module.symbol() -# -# And for classes: -# -# >>> from openfisca_core.module import Symbol -# >>> Symbol() -# -# See: https://www.python.org/dev/peps/pep-0008/#imports - -from .config import ( # noqa: F401 - DAY, - MONTH, - YEAR, - ETERNITY, - INSTANT_PATTERN, - date_by_instant_cache, - str_by_instant_cache, - year_or_month_or_day_re, - ) - -from .helpers import ( # noqa: F401 - instant, - instant_date, - period, - key_period_size, - unit_weights, - unit_weight, - ) - -from .instant_ import Instant # noqa: F401 -from .period_ import Period # noqa: F401 +"""Transitional imports to ensure non-breaking changes. + +These imports could be deprecated in the next major release. + +Currently, imports are used in the following way:: + from openfisca_core.module import symbol + +This example causes cyclic dependency problems, which prevent us from +modularising the different components of the library and make them easier to +test and maintain. + +After the next major release, imports could be used in the following way:: + from openfisca_core import module + module.symbol() + +And for classes:: + from openfisca_core.module import Symbol + Symbol() + +.. 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 + +""" + +from ._config import INSTANT_PATTERN, UNIT_WEIGHTS +from ._utils import key_period_size +from .app._builders import build_instant, build_period +from .app._parsers import parse_period +from .domain._instant import Instant +from .domain._period import Period +from .domain._unit import DateUnit diff --git a/openfisca_core/periods/_config.py b/openfisca_core/periods/_config.py new file mode 100644 index 0000000000..9a8ce29911 --- /dev/null +++ b/openfisca_core/periods/_config.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Pattern + +import re + +from .domain._unit import DateUnit + +# Matches "2015", "2015-01", "2015-01-01" +# Does not match "2015-13", "2015-12-32" +INSTANT_PATTERN: Pattern[str] = re.compile(r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$") + +UNIT_MAPPING = { + 1: DateUnit.YEAR, + 2: DateUnit.MONTH, + 3: DateUnit.DAY, + } + +UNIT_WEIGHTS = { + DateUnit.DAY: 100, + DateUnit.MONTH: 200, + DateUnit.YEAR: 300, + DateUnit.ETERNITY: 400, + } diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py new file mode 100644 index 0000000000..41845da17d --- /dev/null +++ b/openfisca_core/periods/_errors.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from typing import Any + +LEARN_MORE = ( + "Learn more about legal period formats in OpenFisca: " + "." + ) + + +class DateUnitValueError(ValueError): + """Raised when a date unit's value is not valid.""" + + def __init__(self, value: Any) -> None: + super().__init__( + f"'{value}' is not a valid ISO format date unit. ISO format date " + f"units are any of: 'day', 'month', or " + f"'year'. {LEARN_MORE}" + ) + + +class InstantFormatError(ValueError): + """Raised when an instant's format is not valid (ISO format).""" + + def __init__(self, value: Any) -> None: + super().__init__( + f"'{value}' is not a valid instant. Instants are described using " + "the 'YYYY-MM-DD' format, for instance '2015-06-15'. {LEARN_MORE}" + ) + + +class InstantValueError(ValueError): + """Raised when an instant's values are not valid.""" + + def __init__(self, value: Any) -> None: + super().__init__( + f"Invalid instant: '{value}' has a length of {len(value)}. " + "Instants are described using the 'YYYY-MM-DD' format, for " + "instance '2015-06-15', therefore their length has to be within " + f" the following range: 1 <= length <= 3. {LEARN_MORE}" + ) + + +class InstantTypeError(TypeError): + """Raised when an instant's type is not valid.""" + + def __init__(self, value: Any) -> None: + super().__init__( + f"Invalid instant: {value} of type {type(value)}, expecting an " + f"'Instant', 'tuple', or 'list'. {LEARN_MORE}" + ) + + +class PeriodFormatError(ValueError): + """Raised when a period's format is not valid .""" + + def __init__(self, value: Any) -> None: + super().__init__( + f"'{value}' is not a valid period. Periods are described using " + "the 'unit:YYYY-MM-DD:size' format, for instance " + f"'day:2023-01-15:3'. {LEARN_MORE}" + ) + + +class OffsetTypeError(TypeError): + """Raised when an offset's type is not valid.""" + + def __init__(self, value: Any) -> None: + super().__init__( + f"Invalid offset: {value} of type {type(value)}, expecting an " + f"'int'. {LEARN_MORE}" + ) diff --git a/openfisca_core/periods/_utils.py b/openfisca_core/periods/_utils.py new file mode 100644 index 0000000000..cec2a35d5d --- /dev/null +++ b/openfisca_core/periods/_utils.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from ._config import UNIT_WEIGHTS +from .typing import Period + + +def key_period_size(period: Period) -> str: + """Define a key in order to sort periods by length. + + It uses two aspects: first, ``unit``, then, ``size``. + + Args: + period (Period): A Period. + + Returns: + str: A string. + + Examples: + >>> from openfisca_core import periods + + >>> instant = periods.Instant((2021, 9, 14)) + + >>> period = periods.Period((periods.DateUnit.DAY, instant, 1)) + >>> key_period_size(period) + '100_1' + + >>> period = periods.Period((periods.DateUnit.YEAR, instant, 3)) + >>> key_period_size(period) + '300_3' + + """ + + unit, _start, size = period + + return f"{UNIT_WEIGHTS[unit]}_{size}" diff --git a/openfisca_core/periods/app/__init__.py b/openfisca_core/periods/app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/periods/app/_builders.py b/openfisca_core/periods/app/_builders.py new file mode 100644 index 0000000000..87b4f041ee --- /dev/null +++ b/openfisca_core/periods/app/_builders.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +from typing import Any + +import datetime + +from .._config import INSTANT_PATTERN, UNIT_WEIGHTS +from .._errors import InstantFormatError, InstantValueError, PeriodFormatError +from ..domain._instant import Instant +from ..domain._period import Period +from ..domain._unit import DateUnit +from ._parsers import parse_period + + +def build_instant(value: Any) -> Instant | None: + """Build a new instant, aka a triple of integers (year, month, day). + + Args: + value: An ``instant-like`` object. + + Returns: + None: When ``instant`` is None. + :obj:`.Instant`: Otherwise. + + Raises: + InstantFormatError: When the arguments were invalid, like "2021-32-13". + InstantValueError: When the length is out of range. + + Examples: + >>> build_instant(datetime.date(2021, 9, 16)) + Instant((2021, 9, 16)) + + >>> build_instant(Instant((2021, 9, 16))) + Instant((2021, 9, 16)) + + >>> build_instant(Period((DateUnit.YEAR, Instant((2021, 9, 16)), 1))) + Instant((2021, 9, 16)) + + >>> build_instant("2021") + Instant((2021, 1, 1)) + + >>> build_instant(2021) + Instant((2021, 1, 1)) + + >>> build_instant((2021, 9)) + Instant((2021, 9, 1)) + + """ + + if value is None: + return None + + if isinstance(value, Instant): + return value + + if isinstance(value, Period): + return value.start + + if isinstance(value, str) and not INSTANT_PATTERN.match(value): + raise InstantFormatError(value) + + if isinstance(value, str): + instant = tuple(int(fragment) for fragment in value.split("-", 2)[:3]) + + elif isinstance(value, datetime.date): + instant = value.year, value.month, value.day + + elif isinstance(value, int): + instant = value, + + elif isinstance(value, (tuple, list, dict)) and not 1 <= len(value) <= 3: + raise InstantValueError(value) + + else: + instant = tuple(value) + + if len(instant) == 1: + return Instant((instant[0], 1, 1)) + + if len(instant) == 2: + return Instant((instant[0], instant[1], 1)) + + return Instant(instant) + + +def build_period(value: Any) -> Period: + """Build a new period, aka a triple (unit, start_instant, size). + + Args: + value: A ``period-like`` object. + + Returns: + :obj:`.Period`: A period. + + Raises: + PeriodFormatError: When the arguments were invalid, like "2021-32-13". + + Examples: + >>> build_period(Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1))) + Period(('year', Instant((2021, 1, 1)), 1)) + + >>> build_period(Instant((2021, 1, 1))) + Period(('day', Instant((2021, 1, 1)), 1)) + + >>> build_period(DateUnit.ETERNITY) + Period(('eternity', Instant((1, 1, 1)), 1)) + + >>> build_period(2021) + Period(('year', Instant((2021, 1, 1)), 1)) + + >>> build_period("2014") + Period(('year', Instant((2014, 1, 1)), 1)) + + >>> build_period("year:2014") + Period(('year', Instant((2014, 1, 1)), 1)) + + >>> build_period("month:2014-02") + Period(('month', Instant((2014, 2, 1)), 1)) + + >>> build_period("year:2014-02") + Period(('year', Instant((2014, 2, 1)), 1)) + + >>> build_period("day:2014-02-02") + Period(('day', Instant((2014, 2, 2)), 1)) + + >>> build_period("day:2014-02-02:3") + Period(('day', Instant((2014, 2, 2)), 3)) + + """ + + if isinstance(value, Period): + return value + + if isinstance(value, Instant): + return Period((DateUnit.DAY, value, 1)) + + if value == DateUnit.ETERNITY or value == DateUnit.ETERNITY.upper(): + return Period((DateUnit.ETERNITY, build_instant(datetime.date.min), 1)) + + if isinstance(value, int): + return Period((DateUnit.YEAR, Instant((value, 1, 1)), 1)) + + if not isinstance(value, str): + raise PeriodFormatError(value) + + # Try to parse as a simple period + period = parse_period(value) + + if period is not None: + return period + + if ":" not in value: + raise PeriodFormatError(value) + + components = value.split(":") + + # Left-most component must be a valid unit + unit = components[0] + + if unit not in (DateUnit.DAY, DateUnit.MONTH, DateUnit.YEAR): + raise PeriodFormatError(value) + + # Middle component must be a valid iso period + base_period = parse_period(components[1]) + + if not base_period: + raise PeriodFormatError(value) + + # Periods like year:2015-03 have a size of 1 + if len(components) == 2: + size = 1 + + elif len(components) == 3: + try: + size = int(components[2]) + + except ValueError: + raise PeriodFormatError(value) + + # If there are more than 2 ":" in the string, the period is invalid + else: + raise PeriodFormatError(value) + + # Reject ambiguous periods such as month:2014 + if UNIT_WEIGHTS[base_period.unit] > UNIT_WEIGHTS[DateUnit(unit)]: + raise PeriodFormatError(value) + + return Period((unit, base_period.start, size)) diff --git a/openfisca_core/periods/app/_parsers.py b/openfisca_core/periods/app/_parsers.py new file mode 100644 index 0000000000..fd2d46df6e --- /dev/null +++ b/openfisca_core/periods/app/_parsers.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import pendulum +from pendulum import Date +from pendulum.exceptions import ParserError + +from .._config import UNIT_MAPPING +from ..domain._instant import Instant +from ..domain._period import Period + + +def parse_period(value: str) -> Period | None: + """Parse periods respecting the ISO format. + + Args: + value: A string such as such as "2012" or "2015-03". + + Returns: + A Period. + + Raises: + AttributeError: When arguments are invalid, like ``"-1"``. + ValueError: When values are invalid, like ``"2022-32-13"``. + + Examples: + >>> parse_period("2022") + Period(('year', Instant((2022, 1, 1)), 1)) + + >>> parse_period("2022-02") + Period(('month', Instant((2022, 2, 1)), 1)) + + >>> parse_period("2022-02-13") + Period(('day', Instant((2022, 2, 13)), 1)) + + """ + + # If it's a complex period, next! + if len(value.split(":")) != 1: + return None + + # Check for a non-empty string. + if not (value and isinstance(value, str)): + raise AttributeError + + # If it's negative period, next! + if value[0] == "-" or len(value.split(":")) != 1: + raise ValueError + + try: + date = pendulum.parse(value, exact = True) + + except ParserError: + return None + + if not isinstance(date, Date): + raise ValueError + + unit = UNIT_MAPPING[len(value.split("-"))] + + start = Instant((date.year, date.month, date.day)) + + return Period((unit, start, 1)) diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/config.py deleted file mode 100644 index 6e0c698098..0000000000 --- a/openfisca_core/periods/config.py +++ /dev/null @@ -1,15 +0,0 @@ -import re -import typing - -DAY = 'day' -MONTH = 'month' -YEAR = 'year' -ETERNITY = 'eternity' - -# Matches "2015", "2015-01", "2015-01-01" -# Does not match "2015-13", "2015-12-32" -INSTANT_PATTERN = re.compile(r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$") - -date_by_instant_cache: typing.Dict = {} -str_by_instant_cache: typing.Dict = {} -year_or_month_or_day_re = re.compile(r'(18|19|20)\d{2}(-(0?[1-9]|1[0-2])(-([0-2]?\d|3[0-1]))?)?$') diff --git a/openfisca_core/periods/domain/__init__.py b/openfisca_core/periods/domain/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/periods/domain/_instant.py b/openfisca_core/periods/domain/_instant.py new file mode 100644 index 0000000000..a449a5ae6e --- /dev/null +++ b/openfisca_core/periods/domain/_instant.py @@ -0,0 +1,201 @@ +from __future__ import annotations + +from typing import Tuple + +import calendar +import datetime +import functools + +from dateutil import relativedelta + +from openfisca_core.periods._errors import DateUnitValueError, OffsetTypeError +from openfisca_core.periods.domain._unit import DateUnit + + +class Instant(Tuple[int, int, int]): + """An instant in time (``year``, ``month``, ``day``). + + An ``Instant`` represents the most atomic and indivisible + legislation's date unit. + + Current implementation considers this unit to be a day, so + ``instants`` can be thought of as "day dates". + + Args: + (tuple(tuple(int, int, int))): + The ``year``, ``month``, and ``day``, accordingly. + + Examples: + >>> instant = Instant((2021, 9, 13)) + + ``Instants`` are represented as a ``tuple`` containing the date units: + + >>> repr(instant) + 'Instant((2021, 9, 13))' + + However, their user-friendly representation is as a date in the + ISO format: + + >>> str(instant) + '2021-09-13' + + Because ``Instants`` are ``tuples``, they are immutable, which allows + us to use them as keys in hashmaps: + + >>> dict([(instant, (2021, 9, 13))]) + {Instant((2021, 9, 13)): (2021, 9, 13)} + + All the rest of the ``tuple`` protocols are inherited as well: + + >>> instant[0] + 2021 + + >>> instant[0] in instant + True + + >>> len(instant) + 3 + + >>> instant == (2021, 9, 13) + True + + >>> instant > (2020, 9, 13) + True + + >>> year, month, day = instant + + """ + + def __repr__(self) -> str: + return f"{Instant.__name__}({super(Instant, self).__repr__()})" + + @functools.lru_cache(maxsize=None) + def __str__(self) -> str: + return self.date().isoformat() + + @property + def year(self) -> int: + """The ``year`` of the ``Instant``. + + Example: + >>> instant = Instant((2021, 10, 1)) + >>> instant.year + 2021 + + Returns: + An int. + + """ + + return self[0] + + @property + def month(self) -> int: + """The ``month`` of the ``Instant``. + + Example: + >>> instant = Instant((2021, 10, 1)) + >>> instant.month + 10 + + Returns: + An int. + + """ + + return self[1] + + @property + def day(self) -> int: + """The ``day`` of the ``Instant``. + + Example: + >>> instant = Instant((2021, 10, 1)) + >>> instant.day + 1 + + Returns: + An int. + + """ + + return self[2] + + @functools.lru_cache(maxsize=None) + def date(self) -> datetime.date: + """The date representation of the ``Instant``. + + Example: + >>> instant = Instant((2021, 10, 1)) + >>> instant.date() + datetime.date(2021, 10, 1) + + Returns: + A datetime.time. + + """ + + return datetime.date(*self) + + def offset(self, offset: str | int, unit: str) -> Instant: + """Increments/decrements the given instant with offset units. + + Args: + offset (str | int): How much of ``unit`` to offset. + unit (str): What to offset. + + Returns: + Instant: A new one. + + Raises: + DateUnitValueError: When ``unit`` is not a date unit. + OffsetTypeError: When ``offset`` is of type ``int``. + + Examples: + >>> Instant((2020, 12, 31)).offset("first-of", DateUnit.MONTH) + Instant((2020, 12, 1)) + + >>> Instant((2020, 1, 1)).offset("last-of", DateUnit.YEAR) + Instant((2020, 12, 31)) + + >>> Instant((2020, 1, 1)).offset(1, DateUnit.YEAR) + Instant((2021, 1, 1)) + + >>> Instant((2020, 1, 1)).offset(-3, DateUnit.DAY) + Instant((2019, 12, 29)) + + """ + + year, month, day = self + + if unit not in (DateUnit.DAY, DateUnit.MONTH, DateUnit.YEAR): + raise DateUnitValueError(unit) + + if offset == "first-of" and unit == DateUnit.YEAR: + return Instant((year, 1, 1)) + + if offset == "first-of" and unit == DateUnit.MONTH: + return Instant((year, month, 1)) + + if offset == "last-of" and unit == DateUnit.YEAR: + return Instant((year, 12, 31)) + + if offset == "last-of" and unit == DateUnit.MONTH: + return Instant((year, month, calendar.monthrange(year, month)[1])) + + if not isinstance(offset, int): + raise OffsetTypeError(offset) + + if unit == DateUnit.YEAR: + date = self.date() + relativedelta.relativedelta(years=offset) + return Instant((date.year, date.month, date.day)) + + if unit == DateUnit.MONTH: + date = self.date() + relativedelta.relativedelta(months=offset) + return Instant((date.year, date.month, date.day)) + + if unit == DateUnit.DAY: + date = self.date() + relativedelta.relativedelta(days=offset) + return Instant((date.year, date.month, date.day)) + + return self diff --git a/openfisca_core/periods/domain/_period.py b/openfisca_core/periods/domain/_period.py new file mode 100644 index 0000000000..4aca6f2e91 --- /dev/null +++ b/openfisca_core/periods/domain/_period.py @@ -0,0 +1,585 @@ +from __future__ import annotations + +from typing import Sequence, Tuple + +import calendar +import datetime + +from .._config import UNIT_WEIGHTS +from .._errors import DateUnitValueError +from ._instant import Instant +from ._unit import DateUnit + + +class Period(Tuple[DateUnit, Instant, int]): + """Toolbox to handle date intervals. + + A ``Period`` is a triple (``unit``, ``start``, ``size``). + + Attributes: + unit (str): + Either ``year``, ``month``, ``day`` or ``eternity``. + start (:obj:`.Instant`): + The "instant" the :obj:`.Period` starts at. + size (int): + The amount of ``unit``, starting at ``start``, at least ``1``. + + Args: + tuple((str, .Instant, int)): + The ``unit``, ``start``, and ``size``, accordingly. + + Examples: + >>> instant = Instant((2021, 9, 1)) + >>> period = Period((DateUnit.YEAR, instant, 3)) + + >>> repr(Period) + "" + + >>> repr(period) + "Period(('year', Instant((2021, 9, 1)), 3))" + + ``Periods`` are represented as a ``tuple`` containing the ``unit``, + an ``Instant`` and the ``size``: + + >>> repr(period) + "Period(('year', Instant((2021, 9, 1)), 3))" + + Their user-friendly representation is as a date in the + ISO format, prefixed with the ``unit`` and suffixed with its ``size``: + + >>> str(period) + 'year:2021-09:3' + + >>> dict([period, instant]) + Traceback (most recent call last): + ValueError: dictionary update sequence element #0 has length 3... + + >>> list(period) + ['year', Instant((2021, 9, 1)), 3] + + >>> period[0] + 'year' + + However, you won't be able to use them as hashmaps keys. Because they + contain a nested data structure, they're not hashable: + + >>> dict([period, (2021, 9, 13)]) + Traceback (most recent call last): + ValueError: dictionary update sequence element #0 has length 3... + + All the rest of the ``tuple`` protocols are inherited as well: + + >>> period[0] + 'year' + + >>> period[0] in period + True + + >>> len(period) + 3 + + >>> period == Period((DateUnit.YEAR, instant, 3)) + True + + >>> period > Period((DateUnit.YEAR, instant, 3)) + False + + >>> unit, (year, month, day), size = period + + """ + + def __repr__(self) -> str: + return f"{Period.__name__}({super(Period, self).__repr__()})" + + def __str__(self) -> str: + """Transform period to a string. + + Returns: + str: A string representation of the period. + + Examples: + >>> str(Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1))) + '2021' + + >>> str(Period((DateUnit.YEAR, Instant((2021, 2, 1)), 1))) + 'year:2021-02' + + >>> str(Period((DateUnit.MONTH, Instant((2021, 2, 1)), 1))) + '2021-02' + + >>> str(Period((DateUnit.YEAR, Instant((2021, 1, 1)), 2))) + 'year:2021:2' + + >>> str(Period((DateUnit.MONTH, Instant((2021, 1, 1)), 2))) + 'month:2021-01:2' + + >>> str(Period((DateUnit.MONTH, Instant((2021, 1, 1)), 12))) + '2021' + + >>> str(Period((DateUnit.YEAR, Instant((2021, 3, 1)), 2))) + 'year:2021-03:2' + + >>> str(Period((DateUnit.MONTH, Instant((2021, 3, 1)), 2))) + 'month:2021-03:2' + + >>> str(Period((DateUnit.MONTH, Instant((2021, 3, 1)), 12))) + 'year:2021-03' + + """ + + unit, start_instant, size = self + + if unit == DateUnit.ETERNITY: + return DateUnit.ETERNITY.upper() + + year, month, day = start_instant + + # 1 year long period + if unit == DateUnit.MONTH and size == 12 or unit == DateUnit.YEAR and size == 1: + if month == 1: + # civil year starting from january + return str(year) + + else: + # rolling year + return f"{DateUnit.YEAR}:{year}-{month:02d}" + + # simple month + if unit == DateUnit.MONTH and size == 1: + return f"{year}-{month:02d}" + + # several civil years + if unit == DateUnit.YEAR and month == 1: + return f"{unit}:{year}:{size}" + + if unit == DateUnit.DAY: + if size == 1: + return f"{year}-{month:02d}-{day:02d}" + + else: + return f"{unit}:{year}-{month:02d}-{day:02d}:{size}" + + # complex period + return f"{unit}:{year}-{month:02d}:{size}" + + def __contains__(self, other: object) -> bool: + """Checks if a ``period`` contains another one. + + Args: + other (object): The other ``Period``. + + Returns: + True if ``other`` is contained, otherwise False. + + Example: + >>> period = Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)) + >>> sub_period = Period((DateUnit.MONTH, Instant((2021, 1, 1)), 3)) + + >>> sub_period in period + True + + """ + + if isinstance(other, Period): + return self.start <= other.start and self.stop >= other.stop + + return super().__contains__(other) + + @property + def unit(self) -> DateUnit: + """The ``unit`` of the ``Period``. + + Returns: + An int. + + Example: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((DateUnit.YEAR, instant, 3)) + >>> period.unit + 'year' + + """ + + return self[0] + + @property + def days(self) -> int: + """Count the number of days in period. + + Returns: + An int. + + Examples: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((DateUnit.YEAR, instant, 3)) + >>> period.size_in_days + 1096 + + >>> period = Period((DateUnit.MONTH, instant, 3)) + >>> period.size_in_days + 92 + + """ + + return (self.stop.date() - self.start.date()).days + 1 + + @property + def size(self) -> int: + """The ``size`` of the ``Period``. + + Returns: + An int. + + Example: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((DateUnit.YEAR, instant, 3)) + >>> period.size + 3 + + """ + + return self[2] + + @property + def size_in_months(self) -> int: + """The ``size`` of the ``Period`` in months. + + Returns: + An int. + + Raises: + ValueError: If the period's unit is not a month or a year. + + Examples: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((DateUnit.YEAR, instant, 3)) + >>> period.size_in_months + 36 + + >>> period = Period((DateUnit.DAY, instant, 3)) + >>> period.size_in_months + Traceback (most recent call last): + ValueError: Cannot calculate number of months in day. + + """ + + if self[0] == DateUnit.MONTH: + return self[2] + + if self[0] == DateUnit.YEAR: + return self[2] * 12 + + raise ValueError(f"Cannot calculate number of months in {self[0]}.") + + @property + def size_in_days(self) -> int: + """The ``size`` of the ``Period`` in days. + + Returns: + An int. + + Raises: + ValueError: If the period's unit is not a day, a month or a year. + + Examples: + >>> instant = Instant((2019, 10, 1)) + >>> period = Period((DateUnit.YEAR, instant, 3)) + >>> period.size_in_days + 1096 + + >>> period = Period((DateUnit.MONTH, instant, 3)) + >>> period.size_in_days + 92 + + """ + + unit, instant, length = self + + if unit == DateUnit.DAY: + return length + + if unit in [DateUnit.MONTH, DateUnit.YEAR]: + last_day = self.start.offset(length, unit).offset(-1, DateUnit.DAY) + return (last_day.date() - self.start.date()).days + 1 + + raise ValueError(f"Cannot calculate number of days in {unit}") + + @property + def start(self) -> Instant: + """The ``Instant`` at which the ``Period`` starts. + + Returns: + An Instant. + + Example: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((DateUnit.YEAR, instant, 3)) + >>> period.start + Instant((2021, 10, 1)) + + """ + + return self[1] + + @property + def stop(self) -> Instant: + """Last day of the ``Period`` as an ``Instant``. + + Returns: + An Instant. + + Raises: + DateUnitValueError: If the period's unit isn't day, month or year. + + Examples: + >>> Period((DateUnit.YEAR, Instant((2012, 2, 29)), 1)).stop + Instant((2013, 2, 28)) + + >>> Period((DateUnit.MONTH, Instant((2012, 2, 29)), 1)).stop + Instant((2012, 3, 28)) + + >>> Period((DateUnit.DAY, Instant((2012, 2, 29)), 1)).stop + Instant((2012, 2, 29)) + + """ + + unit, start_instant, size = self + year, month, day = start_instant + + if unit == DateUnit.ETERNITY: + return Instant((1, 1, 1)) + + if unit == DateUnit.DAY: + if size > 1: + day += size - 1 + month_last_day = calendar.monthrange(year, month)[1] + + while day > month_last_day: + month += 1 + + if month == 13: + year += 1 + month = 1 + + day -= month_last_day + month_last_day = calendar.monthrange(year, month)[1] + + else: + if unit == DateUnit.MONTH: + month += size + + while month > 12: + year += 1 + month -= 12 + + else: + if not unit == DateUnit.YEAR: + raise DateUnitValueError(unit) + + year += size + + day -= 1 + + if day < 1: + month -= 1 + + if month == 0: + year -= 1 + month = 12 + + day += calendar.monthrange(year, month)[1] + + else: + month_last_day = calendar.monthrange(year, month)[1] + + if day > month_last_day: + month += 1 + + if month == 13: + year += 1 + month = 1 + + day -= month_last_day + + return Instant((year, month, day)) + + @property + def last_month(self) -> Period: + """Last month of the ``Period``. + + Returns: + A Period. + + """ + + return self.first_month.offset(-1) + + @property + def last_3_months(self) -> Period: + """Last 3 months of the ``Period``. + + Returns: + A Period. + + """ + + start: Instant = self.first_month.start + + return Period((DateUnit.MONTH, start, 3)).offset(-3) + + @property + def last_year(self) -> Period: + """Last year of the ``Period``. + + Returns: + A Period. + + """ + + start: Instant = self.start.offset("first-of", DateUnit.YEAR) + + return Period((DateUnit.YEAR, start, 1)).offset(-1) + + @property + def n_2(self) -> Period: + """Last 2 years of the ``Period``. + + Returns: + A Period. + + """ + + start: Instant = self.start.offset("first-of", DateUnit.YEAR) + + return Period((DateUnit.YEAR, start, 1)).offset(-2) + + @property + def this_year(self) -> Period: + """A new year ``Period`` starting at the beginning of the year. + + Returns: + A Period. + + """ + + start: Instant = self.start.offset("first-of", DateUnit.YEAR) + + return Period((DateUnit.YEAR, start, 1)) + + @property + def first_month(self) -> Period: + """A new month ``Period`` starting at the first of the month. + + Returns: + A Period. + + """ + + start: Instant = self.start.offset("first-of", DateUnit.MONTH) + + return Period((DateUnit.MONTH, start, 1)) + + @property + def first_day(self) -> Period: + """A new day ``Period``. + + Returns: + A Period. + + """ + + return Period((DateUnit.DAY, self.start, 1)) + + def date(self) -> datetime.date: + """The date representation of the ``period``'s' start date. + + Returns: + A datetime.date. + + Raises: + ValueError: If the period's size is greater than 1. + + Examples: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((DateUnit.YEAR, instant, 1)) + >>> period.date() + datetime.date(2021, 10, 1) + + >>> period = Period((DateUnit.YEAR, instant, 3)) + >>> period.date() + Traceback (most recent call last): + ValueError: 'date' undefined for period size > 1: year:2021-10:3. + + """ + + if self.size > 1: + raise ValueError(f"'date' undefined for period size > 1: {self}.") + + return self.start.date() + + def offset(self, offset: str | int, unit: str | None = None) -> Period: + """Increment (or decrement) the given period with offset units. + + Args: + offset (str | int): How much of ``unit`` to offset. + unit (str): What to offset. + + Returns: + Period: A new one. + + Examples: + >>> Period((DateUnit.DAY, Instant((2014, 2, 3)), 1)).offset("first-of", "month") + Period(('day', Instant((2014, 2, 1)), 1)) + + >>> Period((DateUnit.MONTH, Instant((2014, 2, 3)), 4)).offset("last-of", "month") + Period(('month', Instant((2014, 2, 28)), 4)) + + >>> Period((DateUnit.DAY, Instant((2021, 1, 1)), 365)).offset(-3) + Period(('day', Instant((2020, 12, 29)), 365)) + + >>> Period((DateUnit.DAY, Instant((2021, 1, 1)), 365)).offset(1, "year") + Period(('day', Instant((2022, 1, 1)), 365)) + + """ + + start = self[1].offset(offset, str(self[0] if unit is None else unit)) + + return Period((self[0], start, self[2])) + + def subperiods(self, unit: str) -> Sequence[Period]: + """Return the list of all the periods of unit ``unit``. + + Args: + unit: A string representing period's ``unit``. + + Returns: + A list of periods. + + Raises: + DateUnitValueError: If the ``unit`` is not a valid date unit. + ValueError: If the period's unit is smaller than the given unit. + + Examples: + >>> period = Period((DateUnit.YEAR, Instant((2021, 1, 1)), 1)) + >>> period.subperiods(DateUnit.MONTH) + [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] + + >>> period = Period((DateUnit.YEAR, Instant((2021, 1, 1)), 2)) + >>> period.subperiods(DateUnit.YEAR) + [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] + + """ + + if UNIT_WEIGHTS[self.unit] < UNIT_WEIGHTS[DateUnit(unit)]: + raise ValueError(f"Cannot subdivide {self.unit} into {unit}") + + if unit == DateUnit.YEAR: + return [self.this_year.offset(i, DateUnit.YEAR) for i in range(self.size)] + + if unit == DateUnit.MONTH: + return [self.first_month.offset(i, DateUnit.MONTH) for i in range(self.size_in_months)] + + if unit == DateUnit.DAY: + return [self.first_day.offset(i, DateUnit.DAY) for i in range(self.size_in_days)] + + raise DateUnitValueError(unit) diff --git a/openfisca_core/periods/domain/_unit.py b/openfisca_core/periods/domain/_unit.py new file mode 100644 index 0000000000..5c04a4ed65 --- /dev/null +++ b/openfisca_core/periods/domain/_unit.py @@ -0,0 +1,60 @@ +import enum + + +class DateUnit(str, enum.Enum): + """The date units of a rule system. + + Examples: + >>> repr(DateUnit) + "" + + >>> repr(DateUnit.DAY) + "'day'" + + >>> str(DateUnit.DAY) + 'day' + + >>> dict([(DateUnit.DAY, DateUnit.DAY.value)]) + {'day': 'day'} + + >>> list(DateUnit) + ['day', 'month', 'year', 'eternity'] + + >>> len(DateUnit) + 4 + + >>> DateUnit["DAY"] + 'day' + + >>> DateUnit(DateUnit.DAY) + 'day' + + >>> DateUnit.DAY in DateUnit + True + + >>> "day" in list(DateUnit) + True + + >>> DateUnit.DAY == "day" + True + + >>> DateUnit.DAY.name + 'DAY' + + >>> DateUnit.DAY.value + 'day' + + .. versionadded:: 39.1.0 + + """ + + DAY = "day" + MONTH = "month" + YEAR = "year" + ETERNITY = "eternity" + + def __repr__(self) -> str: + return self.value.__repr__() + + def __str__(self) -> str: + return self.value.__str__() diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py deleted file mode 100644 index e4f93e4edb..0000000000 --- a/openfisca_core/periods/helpers.py +++ /dev/null @@ -1,202 +0,0 @@ -from typing import Dict - -import datetime -import os - -from . import config -from .instant_ import Instant -from .period_ import Period - - -def instant(instant): - """Return a new instant, aka a triple of integers (year, month, day). - - >>> instant(2014) - Instant((2014, 1, 1)) - >>> instant('2014') - Instant((2014, 1, 1)) - >>> instant('2014-02') - Instant((2014, 2, 1)) - >>> instant('2014-3-2') - Instant((2014, 3, 2)) - >>> instant(instant('2014-3-2')) - Instant((2014, 3, 2)) - >>> instant(period('month', '2014-3-2')) - Instant((2014, 3, 2)) - - >>> instant(None) - """ - if instant is None: - return None - if isinstance(instant, Instant): - return instant - if isinstance(instant, str): - if not config.INSTANT_PATTERN.match(instant): - raise ValueError("'{}' is not a valid instant. Instants are described using the 'YYYY-MM-DD' format, for instance '2015-06-15'.".format(instant)) - instant = Instant( - int(fragment) - for fragment in instant.split('-', 2)[:3] - ) - elif isinstance(instant, datetime.date): - instant = Instant((instant.year, instant.month, instant.day)) - elif isinstance(instant, int): - instant = (instant,) - elif isinstance(instant, list): - assert 1 <= len(instant) <= 3 - instant = tuple(instant) - elif isinstance(instant, Period): - instant = instant.start - else: - assert isinstance(instant, tuple), instant - assert 1 <= len(instant) <= 3 - if len(instant) == 1: - return Instant((instant[0], 1, 1)) - if len(instant) == 2: - return Instant((instant[0], instant[1], 1)) - return Instant(instant) - - -def instant_date(instant): - if instant is None: - return None - instant_date = config.date_by_instant_cache.get(instant) - if instant_date is None: - config.date_by_instant_cache[instant] = instant_date = datetime.date(*instant) - return instant_date - - -def period(value) -> Period: - """Return a new period, aka a triple (unit, start_instant, size). - - >>> period('2014') - Period((YEAR, Instant((2014, 1, 1)), 1)) - >>> period('year:2014') - Period((YEAR, Instant((2014, 1, 1)), 1)) - - >>> period('2014-2') - Period((MONTH, Instant((2014, 2, 1)), 1)) - >>> period('2014-02') - Period((MONTH, Instant((2014, 2, 1)), 1)) - >>> period('month:2014-2') - Period((MONTH, Instant((2014, 2, 1)), 1)) - - >>> period('year:2014-2') - Period((YEAR, Instant((2014, 2, 1)), 1)) - """ - if isinstance(value, Period): - return value - - if isinstance(value, Instant): - return Period((config.DAY, value, 1)) - - def parse_simple_period(value): - """ - Parses simple periods respecting the ISO format, such as 2012 or 2015-03 - """ - try: - date = datetime.datetime.strptime(value, '%Y') - except ValueError: - try: - date = datetime.datetime.strptime(value, '%Y-%m') - except ValueError: - try: - date = datetime.datetime.strptime(value, '%Y-%m-%d') - except ValueError: - return None - else: - return Period((config.DAY, Instant((date.year, date.month, date.day)), 1)) - else: - return Period((config.MONTH, Instant((date.year, date.month, 1)), 1)) - else: - return Period((config.YEAR, Instant((date.year, date.month, 1)), 1)) - - def raise_error(value): - message = os.linesep.join([ - "Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '{}'.".format(value), - "Learn more about legal period formats in OpenFisca:", - "." - ]) - raise ValueError(message) - - if value == 'ETERNITY' or value == config.ETERNITY: - return Period(('eternity', instant(datetime.date.min), float("inf"))) - - # check the type - if isinstance(value, int): - return Period((config.YEAR, Instant((value, 1, 1)), 1)) - if not isinstance(value, str): - raise_error(value) - - # try to parse as a simple period - period = parse_simple_period(value) - if period is not None: - return period - - # complex period must have a ':' in their strings - if ":" not in value: - raise_error(value) - - components = value.split(':') - - # left-most component must be a valid unit - unit = components[0] - if unit not in (config.DAY, config.MONTH, config.YEAR): - raise_error(value) - - # middle component must be a valid iso period - base_period = parse_simple_period(components[1]) - if not base_period: - raise_error(value) - - # period like year:2015-03 have a size of 1 - if len(components) == 2: - size = 1 - # if provided, make sure the size is an integer - elif len(components) == 3: - try: - size = int(components[2]) - except ValueError: - raise_error(value) - # if there is more than 2 ":" in the string, the period is invalid - else: - raise_error(value) - - # reject ambiguous period such as month:2014 - if unit_weight(base_period.unit) > unit_weight(unit): - raise_error(value) - - return Period((unit, base_period.start, size)) - - -def key_period_size(period): - """ - Defines a key in order to sort periods by length. It uses two aspects : first unit then size - - :param period: an OpenFisca period - :return: a string - - >>> key_period_size(period('2014')) - '2_1' - >>> key_period_size(period('2013')) - '2_1' - >>> key_period_size(period('2014-01')) - '1_1' - - """ - - unit, start, size = period - - return '{}_{}'.format(unit_weight(unit), size) - - -def unit_weights() -> Dict[str, int]: - return { - config.DAY: 100, - config.MONTH: 200, - config.YEAR: 300, - config.ETERNITY: 400, - } - - -def unit_weight(unit: str) -> int: - return unit_weights()[unit] diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py deleted file mode 100644 index ad559e5e53..0000000000 --- a/openfisca_core/periods/instant_.py +++ /dev/null @@ -1,233 +0,0 @@ -import calendar -import datetime - -from . import config - - -class Instant(tuple): - - def __repr__(self): - """ - Transform instant to to its Python representation as a string. - - >>> repr(instant(2014)) - 'Instant((2014, 1, 1))' - >>> repr(instant('2014-2')) - 'Instant((2014, 2, 1))' - >>> repr(instant('2014-2-3')) - 'Instant((2014, 2, 3))' - """ - return '{}({})'.format(self.__class__.__name__, super(Instant, self).__repr__()) - - def __str__(self): - """ - Transform instant to a string. - - >>> str(instant(2014)) - '2014-01-01' - >>> str(instant('2014-2')) - '2014-02-01' - >>> str(instant('2014-2-3')) - '2014-02-03' - - """ - instant_str = config.str_by_instant_cache.get(self) - if instant_str is None: - config.str_by_instant_cache[self] = instant_str = self.date.isoformat() - return instant_str - - @property - def date(self): - """ - Convert instant to a date. - - >>> instant(2014).date - datetime.date(2014, 1, 1) - >>> instant('2014-2').date - datetime.date(2014, 2, 1) - >>> instant('2014-2-3').date - datetime.date(2014, 2, 3) - """ - instant_date = config.date_by_instant_cache.get(self) - if instant_date is None: - config.date_by_instant_cache[self] = instant_date = datetime.date(*self) - return instant_date - - @property - def day(self): - """ - Extract day from instant. - - >>> instant(2014).day - 1 - >>> instant('2014-2').day - 1 - >>> instant('2014-2-3').day - 3 - """ - return self[2] - - @property - def month(self): - """ - Extract month from instant. - - >>> instant(2014).month - 1 - >>> instant('2014-2').month - 2 - >>> instant('2014-2-3').month - 2 - """ - return self[1] - - def offset(self, offset, unit): - """ - Increment (or decrement) the given instant with offset units. - - >>> instant(2014).offset(1, 'day') - Instant((2014, 1, 2)) - >>> instant(2014).offset(1, 'month') - Instant((2014, 2, 1)) - >>> instant(2014).offset(1, 'year') - Instant((2015, 1, 1)) - - >>> instant('2014-1-31').offset(1, 'day') - Instant((2014, 2, 1)) - >>> instant('2014-1-31').offset(1, 'month') - Instant((2014, 2, 28)) - >>> instant('2014-1-31').offset(1, 'year') - Instant((2015, 1, 31)) - - >>> instant('2011-2-28').offset(1, 'day') - Instant((2011, 3, 1)) - >>> instant('2011-2-28').offset(1, 'month') - Instant((2011, 3, 28)) - >>> instant('2012-2-29').offset(1, 'year') - Instant((2013, 2, 28)) - - >>> instant(2014).offset(-1, 'day') - Instant((2013, 12, 31)) - >>> instant(2014).offset(-1, 'month') - Instant((2013, 12, 1)) - >>> instant(2014).offset(-1, 'year') - Instant((2013, 1, 1)) - - >>> instant('2011-3-1').offset(-1, 'day') - Instant((2011, 2, 28)) - >>> instant('2011-3-31').offset(-1, 'month') - Instant((2011, 2, 28)) - >>> instant('2012-2-29').offset(-1, 'year') - Instant((2011, 2, 28)) - - >>> instant('2014-1-30').offset(3, 'day') - Instant((2014, 2, 2)) - >>> instant('2014-10-2').offset(3, 'month') - Instant((2015, 1, 2)) - >>> instant('2014-1-1').offset(3, 'year') - Instant((2017, 1, 1)) - - >>> instant(2014).offset(-3, 'day') - Instant((2013, 12, 29)) - >>> instant(2014).offset(-3, 'month') - Instant((2013, 10, 1)) - >>> instant(2014).offset(-3, 'year') - Instant((2011, 1, 1)) - - >>> instant(2014).offset('first-of', 'month') - Instant((2014, 1, 1)) - >>> instant('2014-2').offset('first-of', 'month') - Instant((2014, 2, 1)) - >>> instant('2014-2-3').offset('first-of', 'month') - Instant((2014, 2, 1)) - - >>> instant(2014).offset('first-of', 'year') - Instant((2014, 1, 1)) - >>> instant('2014-2').offset('first-of', 'year') - Instant((2014, 1, 1)) - >>> instant('2014-2-3').offset('first-of', 'year') - Instant((2014, 1, 1)) - - >>> instant(2014).offset('last-of', 'month') - Instant((2014, 1, 31)) - >>> instant('2014-2').offset('last-of', 'month') - Instant((2014, 2, 28)) - >>> instant('2012-2-3').offset('last-of', 'month') - Instant((2012, 2, 29)) - - >>> instant(2014).offset('last-of', 'year') - Instant((2014, 12, 31)) - >>> instant('2014-2').offset('last-of', 'year') - Instant((2014, 12, 31)) - >>> instant('2014-2-3').offset('last-of', 'year') - Instant((2014, 12, 31)) - """ - year, month, day = self - assert unit in (config.DAY, config.MONTH, config.YEAR), 'Invalid unit: {} of type {}'.format(unit, type(unit)) - if offset == 'first-of': - if unit == config.MONTH: - day = 1 - elif unit == config.YEAR: - month = 1 - day = 1 - elif offset == 'last-of': - if unit == config.MONTH: - day = calendar.monthrange(year, month)[1] - elif unit == config.YEAR: - month = 12 - day = 31 - else: - assert isinstance(offset, int), 'Invalid offset: {} of type {}'.format(offset, type(offset)) - if unit == config.DAY: - day += offset - if offset < 0: - while day < 1: - month -= 1 - if month == 0: - year -= 1 - month = 12 - day += calendar.monthrange(year, month)[1] - elif offset > 0: - month_last_day = calendar.monthrange(year, month)[1] - while day > month_last_day: - month += 1 - if month == 13: - year += 1 - month = 1 - day -= month_last_day - month_last_day = calendar.monthrange(year, month)[1] - elif unit == config.MONTH: - month += offset - if offset < 0: - while month < 1: - year -= 1 - month += 12 - elif offset > 0: - while month > 12: - year += 1 - month -= 12 - month_last_day = calendar.monthrange(year, month)[1] - if day > month_last_day: - day = month_last_day - elif unit == config.YEAR: - year += offset - # Handle february month of leap year. - month_last_day = calendar.monthrange(year, month)[1] - if day > month_last_day: - day = month_last_day - - return self.__class__((year, month, day)) - - @property - def year(self): - """ - Extract year from instant. - - >>> instant(2014).year - 2014 - >>> instant('2014-2').year - 2014 - >>> instant('2014-2-3').year - 2014 - """ - return self[0] diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py deleted file mode 100644 index 7de0459bdf..0000000000 --- a/openfisca_core/periods/period_.py +++ /dev/null @@ -1,489 +0,0 @@ -from __future__ import annotations - -import calendar - -from . import config, helpers -from .instant_ import Instant - - -class Period(tuple): - """ - Toolbox to handle date intervals. - - A period is a triple (unit, start, size), where unit is either "month" or "year", where start format is a - (year, month, day) triple, and where size is an integer > 1. - - - Since a period is a triple it can be used as a dictionary key. - """ - - def __repr__(self): - """ - Transform period to to its Python representation as a string. - - >>> repr(period('year', 2014)) - "Period(('year', Instant((2014, 1, 1)), 1))" - >>> repr(period('month', '2014-2')) - "Period(('month', Instant((2014, 2, 1)), 1))" - >>> repr(period('day', '2014-2-3')) - "Period(('day', Instant((2014, 2, 3)), 1))" - """ - return '{}({})'.format(self.__class__.__name__, super(Period, self).__repr__()) - - def __str__(self): - """ - Transform period to a string. - - >>> str(period(YEAR, 2014)) - '2014' - - >>> str(period(YEAR, '2014-2')) - 'year:2014-02' - >>> str(period(MONTH, '2014-2')) - '2014-02' - - >>> str(period(YEAR, 2012, size = 2)) - 'year:2012:2' - >>> str(period(MONTH, 2012, size = 2)) - 'month:2012-01:2' - >>> str(period(MONTH, 2012, size = 12)) - '2012' - - >>> str(period(YEAR, '2012-3', size = 2)) - 'year:2012-03:2' - >>> str(period(MONTH, '2012-3', size = 2)) - 'month:2012-03:2' - >>> str(period(MONTH, '2012-3', size = 12)) - 'year:2012-03' - """ - - unit, start_instant, size = self - if unit == config.ETERNITY: - return 'ETERNITY' - year, month, day = start_instant - - # 1 year long period - if (unit == config.MONTH and size == 12 or unit == config.YEAR and size == 1): - if month == 1: - # civil year starting from january - return str(year) - else: - # rolling year - return '{}:{}-{:02d}'.format(config.YEAR, year, month) - # simple month - if unit == config.MONTH and size == 1: - return '{}-{:02d}'.format(year, month) - # several civil years - if unit == config.YEAR and month == 1: - return '{}:{}:{}'.format(unit, year, size) - - if unit == config.DAY: - if size == 1: - return '{}-{:02d}-{:02d}'.format(year, month, day) - else: - return '{}:{}-{:02d}-{:02d}:{}'.format(unit, year, month, day, size) - - # complex period - return '{}:{}-{:02d}:{}'.format(unit, year, month, size) - - @property - def date(self): - assert self.size == 1, '"date" is undefined for a period of size > 1: {}'.format(self) - return self.start.date - - @property - def days(self): - """ - Count the number of days in period. - - >>> period('day', 2014).days - 365 - >>> period('month', 2014).days - 365 - >>> period('year', 2014).days - 365 - - >>> period('day', '2014-2').days - 28 - >>> period('month', '2014-2').days - 28 - >>> period('year', '2014-2').days - 365 - - >>> period('day', '2014-2-3').days - 1 - >>> period('month', '2014-2-3').days - 28 - >>> period('year', '2014-2-3').days - 365 - """ - return (self.stop.date - self.start.date).days + 1 - - def intersection(self, start, stop): - if start is None and stop is None: - return self - period_start = self[1] - period_stop = self.stop - if start is None: - start = period_start - if stop is None: - stop = period_stop - if stop < period_start or period_stop < start: - return None - intersection_start = max(period_start, start) - intersection_stop = min(period_stop, stop) - if intersection_start == period_start and intersection_stop == period_stop: - return self - if intersection_start.day == 1 and intersection_start.month == 1 \ - and intersection_stop.day == 31 and intersection_stop.month == 12: - return self.__class__(( - 'year', - intersection_start, - intersection_stop.year - intersection_start.year + 1, - )) - if intersection_start.day == 1 and intersection_stop.day == calendar.monthrange(intersection_stop.year, - intersection_stop.month)[1]: - return self.__class__(( - 'month', - intersection_start, - ( - (intersection_stop.year - intersection_start.year) * 12 - + intersection_stop.month - - intersection_start.month - + 1 - ), - )) - return self.__class__(( - 'day', - intersection_start, - (intersection_stop.date - intersection_start.date).days + 1, - )) - - def get_subperiods(self, unit): - """ - Return the list of all the periods of unit ``unit`` contained in self. - - Examples: - - >>> period('2017').get_subperiods(MONTH) - >>> [period('2017-01'), period('2017-02'), ... period('2017-12')] - - >>> period('year:2014:2').get_subperiods(YEAR) - >>> [period('2014'), period('2015')] - """ - if helpers.unit_weight(self.unit) < helpers.unit_weight(unit): - raise ValueError('Cannot subdivide {0} into {1}'.format(self.unit, unit)) - - if unit == config.YEAR: - return [self.this_year.offset(i, config.YEAR) for i in range(self.size)] - - if unit == config.MONTH: - return [self.first_month.offset(i, config.MONTH) for i in range(self.size_in_months)] - - if unit == config.DAY: - return [self.first_day.offset(i, config.DAY) for i in range(self.size_in_days)] - - def offset(self, offset, unit = None): - """ - Increment (or decrement) the given period with offset units. - - >>> period('day', 2014).offset(1) - Period(('day', Instant((2014, 1, 2)), 365)) - >>> period('day', 2014).offset(1, 'day') - Period(('day', Instant((2014, 1, 2)), 365)) - >>> period('day', 2014).offset(1, 'month') - Period(('day', Instant((2014, 2, 1)), 365)) - >>> period('day', 2014).offset(1, 'year') - Period(('day', Instant((2015, 1, 1)), 365)) - - >>> period('month', 2014).offset(1) - Period(('month', Instant((2014, 2, 1)), 12)) - >>> period('month', 2014).offset(1, 'day') - Period(('month', Instant((2014, 1, 2)), 12)) - >>> period('month', 2014).offset(1, 'month') - Period(('month', Instant((2014, 2, 1)), 12)) - >>> period('month', 2014).offset(1, 'year') - Period(('month', Instant((2015, 1, 1)), 12)) - - >>> period('year', 2014).offset(1) - Period(('year', Instant((2015, 1, 1)), 1)) - >>> period('year', 2014).offset(1, 'day') - Period(('year', Instant((2014, 1, 2)), 1)) - >>> period('year', 2014).offset(1, 'month') - Period(('year', Instant((2014, 2, 1)), 1)) - >>> period('year', 2014).offset(1, 'year') - Period(('year', Instant((2015, 1, 1)), 1)) - - >>> period('day', '2011-2-28').offset(1) - Period(('day', Instant((2011, 3, 1)), 1)) - >>> period('month', '2011-2-28').offset(1) - Period(('month', Instant((2011, 3, 28)), 1)) - >>> period('year', '2011-2-28').offset(1) - Period(('year', Instant((2012, 2, 28)), 1)) - - >>> period('day', '2011-3-1').offset(-1) - Period(('day', Instant((2011, 2, 28)), 1)) - >>> period('month', '2011-3-1').offset(-1) - Period(('month', Instant((2011, 2, 1)), 1)) - >>> period('year', '2011-3-1').offset(-1) - Period(('year', Instant((2010, 3, 1)), 1)) - - >>> period('day', '2014-1-30').offset(3) - Period(('day', Instant((2014, 2, 2)), 1)) - >>> period('month', '2014-1-30').offset(3) - Period(('month', Instant((2014, 4, 30)), 1)) - >>> period('year', '2014-1-30').offset(3) - Period(('year', Instant((2017, 1, 30)), 1)) - - >>> period('day', 2014).offset(-3) - Period(('day', Instant((2013, 12, 29)), 365)) - >>> period('month', 2014).offset(-3) - Period(('month', Instant((2013, 10, 1)), 12)) - >>> period('year', 2014).offset(-3) - Period(('year', Instant((2011, 1, 1)), 1)) - - >>> period('day', '2014-2-3').offset('first-of', 'month') - Period(('day', Instant((2014, 2, 1)), 1)) - >>> period('day', '2014-2-3').offset('first-of', 'year') - Period(('day', Instant((2014, 1, 1)), 1)) - - >>> period('day', '2014-2-3', 4).offset('first-of', 'month') - Period(('day', Instant((2014, 2, 1)), 4)) - >>> period('day', '2014-2-3', 4).offset('first-of', 'year') - Period(('day', Instant((2014, 1, 1)), 4)) - - >>> period('month', '2014-2-3').offset('first-of') - Period(('month', Instant((2014, 2, 1)), 1)) - >>> period('month', '2014-2-3').offset('first-of', 'month') - Period(('month', Instant((2014, 2, 1)), 1)) - >>> period('month', '2014-2-3').offset('first-of', 'year') - Period(('month', Instant((2014, 1, 1)), 1)) - - >>> period('month', '2014-2-3', 4).offset('first-of') - Period(('month', Instant((2014, 2, 1)), 4)) - >>> period('month', '2014-2-3', 4).offset('first-of', 'month') - Period(('month', Instant((2014, 2, 1)), 4)) - >>> period('month', '2014-2-3', 4).offset('first-of', 'year') - Period(('month', Instant((2014, 1, 1)), 4)) - - >>> period('year', 2014).offset('first-of') - Period(('year', Instant((2014, 1, 1)), 1)) - >>> period('year', 2014).offset('first-of', 'month') - Period(('year', Instant((2014, 1, 1)), 1)) - >>> period('year', 2014).offset('first-of', 'year') - Period(('year', Instant((2014, 1, 1)), 1)) - - >>> period('year', '2014-2-3').offset('first-of') - Period(('year', Instant((2014, 1, 1)), 1)) - >>> period('year', '2014-2-3').offset('first-of', 'month') - Period(('year', Instant((2014, 2, 1)), 1)) - >>> period('year', '2014-2-3').offset('first-of', 'year') - Period(('year', Instant((2014, 1, 1)), 1)) - - >>> period('day', '2014-2-3').offset('last-of', 'month') - Period(('day', Instant((2014, 2, 28)), 1)) - >>> period('day', '2014-2-3').offset('last-of', 'year') - Period(('day', Instant((2014, 12, 31)), 1)) - - >>> period('day', '2014-2-3', 4).offset('last-of', 'month') - Period(('day', Instant((2014, 2, 28)), 4)) - >>> period('day', '2014-2-3', 4).offset('last-of', 'year') - Period(('day', Instant((2014, 12, 31)), 4)) - - >>> period('month', '2014-2-3').offset('last-of') - Period(('month', Instant((2014, 2, 28)), 1)) - >>> period('month', '2014-2-3').offset('last-of', 'month') - Period(('month', Instant((2014, 2, 28)), 1)) - >>> period('month', '2014-2-3').offset('last-of', 'year') - Period(('month', Instant((2014, 12, 31)), 1)) - - >>> period('month', '2014-2-3', 4).offset('last-of') - Period(('month', Instant((2014, 2, 28)), 4)) - >>> period('month', '2014-2-3', 4).offset('last-of', 'month') - Period(('month', Instant((2014, 2, 28)), 4)) - >>> period('month', '2014-2-3', 4).offset('last-of', 'year') - Period(('month', Instant((2014, 12, 31)), 4)) - - >>> period('year', 2014).offset('last-of') - Period(('year', Instant((2014, 12, 31)), 1)) - >>> period('year', 2014).offset('last-of', 'month') - Period(('year', Instant((2014, 1, 31)), 1)) - >>> period('year', 2014).offset('last-of', 'year') - Period(('year', Instant((2014, 12, 31)), 1)) - - >>> period('year', '2014-2-3').offset('last-of') - Period(('year', Instant((2014, 12, 31)), 1)) - >>> period('year', '2014-2-3').offset('last-of', 'month') - Period(('year', Instant((2014, 2, 28)), 1)) - >>> period('year', '2014-2-3').offset('last-of', 'year') - Period(('year', Instant((2014, 12, 31)), 1)) - """ - return self.__class__((self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2])) - - def contains(self, other: Period) -> bool: - """ - Returns ``True`` if the period contains ``other``. For instance, ``period(2015)`` contains ``period(2015-01)`` - """ - return self.start <= other.start and self.stop >= other.stop - - @property - def size(self): - """ - Return the size of the period. - - >>> period('month', '2012-2-29', 4).size - 4 - """ - return self[2] - - @property - def size_in_months(self): - """ - Return the size of the period in months. - - >>> period('month', '2012-2-29', 4).size_in_months - 4 - >>> period('year', '2012', 1).size_in_months - 12 - """ - if (self[0] == config.MONTH): - return self[2] - if(self[0] == config.YEAR): - return self[2] * 12 - raise ValueError("Cannot calculate number of months in {0}".format(self[0])) - - @property - def size_in_days(self): - """ - Return the size of the period in days. - - >>> period('month', '2012-2-29', 4).size_in_days - 28 - >>> period('year', '2012', 1).size_in_days - 366 - """ - unit, instant, length = self - - if unit == config.DAY: - return length - if unit in [config.MONTH, config.YEAR]: - last_day = self.start.offset(length, unit).offset(-1, config.DAY) - return (last_day.date - self.start.date).days + 1 - - raise ValueError("Cannot calculate number of days in {0}".format(unit)) - - @property - def start(self) -> Instant: - """ - Return the first day of the period as an Instant instance. - - >>> period('month', '2012-2-29', 4).start - Instant((2012, 2, 29)) - """ - return self[1] - - @property - def stop(self) -> Instant: - """ - Return the last day of the period as an Instant instance. - - >>> period('year', 2014).stop - Instant((2014, 12, 31)) - >>> period('month', 2014).stop - Instant((2014, 12, 31)) - >>> period('day', 2014).stop - Instant((2014, 12, 31)) - - >>> period('year', '2012-2-29').stop - Instant((2013, 2, 28)) - >>> period('month', '2012-2-29').stop - Instant((2012, 3, 28)) - >>> period('day', '2012-2-29').stop - Instant((2012, 2, 29)) - - >>> period('year', '2012-2-29', 2).stop - Instant((2014, 2, 28)) - >>> period('month', '2012-2-29', 2).stop - Instant((2012, 4, 28)) - >>> period('day', '2012-2-29', 2).stop - Instant((2012, 3, 1)) - """ - unit, start_instant, size = self - year, month, day = start_instant - if unit == config.ETERNITY: - return Instant((float("inf"), float("inf"), float("inf"))) - if unit == 'day': - if size > 1: - day += size - 1 - month_last_day = calendar.monthrange(year, month)[1] - while day > month_last_day: - month += 1 - if month == 13: - year += 1 - month = 1 - day -= month_last_day - month_last_day = calendar.monthrange(year, month)[1] - else: - if unit == 'month': - month += size - while month > 12: - year += 1 - month -= 12 - else: - assert unit == 'year', 'Invalid unit: {} of type {}'.format(unit, type(unit)) - year += size - day -= 1 - if day < 1: - month -= 1 - if month == 0: - year -= 1 - month = 12 - day += calendar.monthrange(year, month)[1] - else: - month_last_day = calendar.monthrange(year, month)[1] - if day > month_last_day: - month += 1 - if month == 13: - year += 1 - month = 1 - day -= month_last_day - return Instant((year, month, day)) - - @property - def unit(self) -> str: - return self[0] - - # Reference periods - - @property - def last_month(self) -> Period: - return self.first_month.offset(-1) - - @property - def last_3_months(self) -> Period: - start: Instant = self.first_month.start - return self.__class__((config.MONTH, start, 3)).offset(-3) - - @property - def last_year(self) -> Period: - start: Instant = self.start.offset("first-of", config.YEAR) - return self.__class__((config.YEAR, start, 1)).offset(-1) - - @property - def n_2(self) -> Period: - start: Instant = self.start.offset("first-of", config.YEAR) - return self.__class__((config.YEAR, start, 1)).offset(-2) - - @property - def this_year(self) -> Period: - start: Instant = self.start.offset("first-of", config.YEAR) - return self.__class__((config.YEAR, start, 1)) - - @property - def first_month(self) -> Period: - start: Instant = self.start.offset("first-of", config.MONTH) - return self.__class__((config.MONTH, start, 1)) - - @property - def first_day(self) -> Period: - return self.__class__((config.DAY, self.start, 1)) diff --git a/openfisca_core/periods/py.typed b/openfisca_core/periods/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/periods/tests/__init__.py b/openfisca_core/periods/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/periods/tests/app/__init__.py b/openfisca_core/periods/tests/app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/periods/tests/app/test_build_instant.py b/openfisca_core/periods/tests/app/test_build_instant.py new file mode 100644 index 0000000000..3c2f2b2263 --- /dev/null +++ b/openfisca_core/periods/tests/app/test_build_instant.py @@ -0,0 +1,79 @@ +import datetime + +import pytest + +from openfisca_core import periods + + +@pytest.mark.parametrize("arg, expected", [ + ["1000", periods.Instant((1000, 1, 1))], + ["1000", periods.Instant((1000, 1, 1))], + ["1000-01", periods.Instant((1000, 1, 1))], + ["1000-01", periods.Instant((1000, 1, 1))], + ["1000-01-01", periods.Instant((1000, 1, 1))], + ["1000-01-01", periods.Instant((1000, 1, 1))], + [("-1", "-1"), periods.Instant(("-1", "-1", 1))], + [("-1", "-1", "-1"), periods.Instant(("-1", "-1", "-1"))], + [("-1",), periods.Instant(("-1", 1, 1))], + [("1-1",), periods.Instant(("1-1", 1, 1))], + [("1-1-1",), periods.Instant(("1-1-1", 1, 1))], + [(-1, -1), periods.Instant((-1, -1, 1))], + [(-1, -1, -1), periods.Instant((-1, -1, -1))], + [(-1,), periods.Instant((-1, 1, 1))], + [(None, None), periods.Instant((None, None, 1))], + [(None, None, None), periods.Instant((None, None, None))], + [(None,), periods.Instant((None, 1, 1))], + [(datetime.date(1, 1, 1),), periods.Instant((datetime.date(1, 1, 1), 1, 1))], + [(periods.Instant((1, 1, 1)),), periods.Instant((periods.Instant((1, 1, 1)), 1, 1))], + [(periods.Period((periods.DateUnit.DAY, periods.Instant((1, 1, 1)), 365)),), periods.Instant((periods.Period((periods.DateUnit.DAY, periods.Instant((1, 1, 1)), 365)), 1, 1))], + [-1, periods.Instant((-1, 1, 1))], + [0, periods.Instant((0, 1, 1))], + [1, periods.Instant((1, 1, 1))], + [1000, periods.Instant((1000, 1, 1))], + [999, periods.Instant((999, 1, 1))], + [None, None], + [datetime.date(1, 1, 1), periods.Instant((1, 1, 1))], + [periods.Instant((1, 1, 1)), periods.Instant((1, 1, 1))], + [periods.Period((periods.DateUnit.DAY, periods.Instant((1, 1, 1)), 365)), periods.Instant((1, 1, 1))], + ]) +def test_build_instant(arg, expected): + """Returns the expected ``Instant``.""" + + assert periods.build_instant(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + ["", ValueError], + ["1", ValueError], + ["1000-0", ValueError], + ["1000-0-0", ValueError], + ["1000-01-0", ValueError], + ["1000-01-01:1", ValueError], + ["1000-01-01:a", ValueError], + ["1000-01-1", ValueError], + ["1000-01-32", ValueError], + ["1000-1", ValueError], + ["1000-1-1", ValueError], + ["1000-13", ValueError], + ["1:1000-01-01", ValueError], + ["999", ValueError], + ["a", ValueError], + ["a:1000-01-01", ValueError], + ["eternity", ValueError], + ["month:1000", ValueError], + ["month:1000:1", ValueError], + ["year", ValueError], + ["year:1000-01-01", ValueError], + ["year:1000-01-01:1", ValueError], + ["year:1000-01-01:3", ValueError], + [(), ValueError], + [(None, None, None, None), ValueError], + [periods.DateUnit.ETERNITY, ValueError], + [periods.DateUnit.YEAR, ValueError], + [{}, ValueError], + ]) +def test_build_instant_with_an_invalid_argument(arg, error): + """Raises ``ValueError`` when given an invalid argument.""" + + with pytest.raises(error): + periods.build_instant(arg) diff --git a/openfisca_core/periods/tests/app/test_build_period.py b/openfisca_core/periods/tests/app/test_build_period.py new file mode 100644 index 0000000000..d8aa22ac8d --- /dev/null +++ b/openfisca_core/periods/tests/app/test_build_period.py @@ -0,0 +1,94 @@ +import datetime + +import pytest + +from openfisca_core import periods + + +@pytest.mark.parametrize("arg, expected", [ + ["1000", periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["1000-01", periods.Period((periods.DateUnit.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["1000-01-01", periods.Period((periods.DateUnit.DAY, periods.Instant((1000, 1, 1)), 1))], + ["1004-02-29", periods.Period((periods.DateUnit.DAY, periods.Instant((1004, 2, 29)), 1))], + ["ETERNITY", periods.Period((periods.DateUnit.ETERNITY, periods.Instant((1, 1, 1)), 1))], + ["day:1000-01-01", periods.Period((periods.DateUnit.DAY, periods.Instant((1000, 1, 1)), 1))], + ["day:1000-01-01:3", periods.Period((periods.DateUnit.DAY, periods.Instant((1000, 1, 1)), 3))], + ["eternity", periods.Period((periods.DateUnit.ETERNITY, periods.Instant((1, 1, 1)), 1))], + ["month:1000-01", periods.Period((periods.DateUnit.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["month:1000-01-01", periods.Period((periods.DateUnit.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["month:1000-01-01:3", periods.Period((periods.DateUnit.MONTH, periods.Instant((1000, 1, 1)), 3))], + ["month:1000-01:3", periods.Period((periods.DateUnit.MONTH, periods.Instant((1000, 1, 1)), 3))], + ["year:1000", periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["year:1000-01", periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["year:1000-01-01", periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["year:1000-01-01:3", periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 3))], + ["year:1000-01:3", periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 3))], + ["year:1000:3", periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 3))], + [-1, periods.Period((periods.DateUnit.YEAR, periods.Instant((-1, 1, 1)), 1))], + [0, periods.Period((periods.DateUnit.YEAR, periods.Instant((0, 1, 1)), 1))], + [1, periods.Period((periods.DateUnit.YEAR, periods.Instant((1, 1, 1)), 1))], + [1000, periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 1))], + [1000, periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 1))], + [999, periods.Period((periods.DateUnit.YEAR, periods.Instant((999, 1, 1)), 1))], + [periods.DateUnit.ETERNITY, periods.Period((periods.DateUnit.ETERNITY, periods.Instant((1, 1, 1)), 1))], + [periods.Instant((1, 1, 1)), periods.Period((periods.DateUnit.DAY, periods.Instant((1, 1, 1)), 1))], + [periods.Period((periods.DateUnit.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.DateUnit.DAY, periods.Instant((1, 1, 1)), 365))], + ]) +def test_build_period(arg, expected): + """Returns the expected ``Period``.""" + assert periods.build_period(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + ["", AttributeError], + ["1", ValueError], + ["1000-0", ValueError], + ["1000-0-0", ValueError], + ["1000-01-01:1", ValueError], + ["1000-01:1", ValueError], + ["1000-1", ValueError], + ["1000-1-0", ValueError], + ["1000-1-1", ValueError], + ["1000-13", ValueError], + ["1000-2-31", ValueError], + ["1000:1", ValueError], + ["1000:a", ValueError], + ["1:1000", ValueError], + ["999", ValueError], + ["a", ValueError], + ["a:1000", ValueError], + ["day:1000-01", ValueError], + ["day:1000-01:1", ValueError], + ["day:1000:1", ValueError], + ["month:1000", ValueError], + ["month:1000:1", ValueError], + ["year", ValueError], + [("-1", "-1"), ValueError], + [("-1", "-1", "-1"), ValueError], + [("-1",), ValueError], + [("1-1",), ValueError], + [("1-1-1",), ValueError], + [(), ValueError], + [(-1, -1), ValueError], + [(-1, -1, -1), ValueError], + [(-1,), ValueError], + [(1, 1), ValueError], + [(1, 1, 1), ValueError], + [(1,), ValueError], + [(None, None), ValueError], + [(None, None, None), ValueError], + [(None, None, None, None), ValueError], + [(None,), ValueError], + [(datetime.date(1, 1, 1),), ValueError], + [(periods.Instant((1, 1, 1)),), ValueError], + [(periods.Period((periods.DateUnit.DAY, periods.Instant((1, 1, 1)), 365)),), ValueError], + [None, ValueError], + [datetime.date(1, 1, 1), ValueError], + [periods.DateUnit.YEAR, ValueError], + [{}, ValueError], + ]) +def test_build_period_with_an_invalid_argument(arg, error): + """Raises ``ValueError`` when given an invalid argument.""" + + with pytest.raises(error): + periods.build_period(arg) diff --git a/openfisca_core/periods/tests/app/test_parsers.py b/openfisca_core/periods/tests/app/test_parsers.py new file mode 100644 index 0000000000..a315a0948d --- /dev/null +++ b/openfisca_core/periods/tests/app/test_parsers.py @@ -0,0 +1,20 @@ +import pytest + +from openfisca_core import periods + + +@pytest.mark.parametrize("arg, expected", [ + ["1", None], + ["1000", periods.Period((periods.DateUnit.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["1000-01", periods.Period((periods.DateUnit.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["1000-01-01", periods.Period((periods.DateUnit.DAY, periods.Instant((1000, 1, 1)), 1))], + ["1000-01-1", None], + ["1000-01-99", None], + ["1000-1", None], + ["1000-1-1", None], + ["999", None], + ]) +def test_parse_period(arg, expected): + """Returns an ``Instant`` when given a valid ISO format string.""" + + assert periods.parse_period(arg) == expected diff --git a/openfisca_core/periods/tests/domain/__init__.py b/openfisca_core/periods/tests/domain/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/periods/tests/domain/test_instant.py b/openfisca_core/periods/tests/domain/test_instant.py new file mode 100644 index 0000000000..99805e2664 --- /dev/null +++ b/openfisca_core/periods/tests/domain/test_instant.py @@ -0,0 +1,37 @@ +import pytest + +from openfisca_core import periods + + +@pytest.fixture +def instant(): + """Returns a ``Instant``.""" + return periods.Instant((2020, 2, 29)) + + +@pytest.mark.parametrize("offset, unit, expected", [ + ["first-of", periods.DateUnit.YEAR, periods.Instant((2020, 1, 1))], + ["first-of", periods.DateUnit.MONTH, periods.Instant((2020, 2, 1))], + ["last-of", periods.DateUnit.YEAR, periods.Instant((2020, 12, 31))], + ["last-of", periods.DateUnit.MONTH, periods.Instant((2020, 2, 29))], + [-3, periods.DateUnit.YEAR, periods.Instant((2017, 2, 28))], + [-3, periods.DateUnit.MONTH, periods.Instant((2019, 11, 29))], + [-3, periods.DateUnit.DAY, periods.Instant((2020, 2, 26))], + [3, periods.DateUnit.YEAR, periods.Instant((2023, 2, 28))], + [3, periods.DateUnit.MONTH, periods.Instant((2020, 5, 29))], + [3, periods.DateUnit.DAY, periods.Instant((2020, 3, 3))], + ]) +def test_offset(instant, offset, unit, expected): + """Returns the expected ``Instant``.""" + assert instant.offset(offset, unit) == expected + + +@pytest.mark.parametrize("offset, unit, expected", [ + ["first-of", periods.DateUnit.DAY, TypeError], + ["last-of", periods.DateUnit.DAY, TypeError], + ]) +def test_offset_with_an_invalid_offset(instant, offset, unit, expected): + """Raises ``OffsetTypeError`` when given an invalid offset.""" + + with pytest.raises(TypeError): + instant.offset(offset, unit) diff --git a/openfisca_core/periods/tests/domain/test_period.py b/openfisca_core/periods/tests/domain/test_period.py new file mode 100644 index 0000000000..dbcc89e974 --- /dev/null +++ b/openfisca_core/periods/tests/domain/test_period.py @@ -0,0 +1,131 @@ +import pytest + +from openfisca_core import periods + + +@pytest.fixture +def instant(): + """Returns a ``Instant``.""" + return periods.Instant((2022, 12, 31)) + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.DateUnit.YEAR, periods.Instant((2022, 1, 1)), 1, "2022"], + [periods.DateUnit.MONTH, periods.Instant((2022, 1, 1)), 12, "2022"], + [periods.DateUnit.YEAR, periods.Instant((2022, 3, 1)), 1, "year:2022-03"], + [periods.DateUnit.MONTH, periods.Instant((2022, 3, 1)), 12, "year:2022-03"], + [periods.DateUnit.YEAR, periods.Instant((2022, 1, 1)), 3, "year:2022:3"], + [periods.DateUnit.YEAR, periods.Instant((2022, 1, 3)), 3, "year:2022:3"], + ]) +def test_str_with_years(date_unit, instant, size, expected): + """Returns the expected string.""" + + assert str(periods.Period((date_unit, instant, size))) == expected + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.DateUnit.MONTH, periods.Instant((2022, 1, 1)), 1, "2022-01"], + [periods.DateUnit.MONTH, periods.Instant((2022, 1, 1)), 3, "month:2022-01:3"], + [periods.DateUnit.MONTH, periods.Instant((2022, 3, 1)), 3, "month:2022-03:3"], + ]) +def test_str_with_months(date_unit, instant, size, expected): + """Returns the expected string.""" + + assert str(periods.Period((date_unit, instant, size))) == expected + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.DateUnit.DAY, periods.Instant((2022, 1, 1)), 1, "2022-01-01"], + [periods.DateUnit.DAY, periods.Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], + [periods.DateUnit.DAY, periods.Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], + ]) +def test_str_with_days(date_unit, instant, size, expected): + """Returns the expected string.""" + + assert str(periods.Period((date_unit, instant, size))) == expected + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.DateUnit.MONTH, periods.Instant((2022, 12, 1)), 1, 1], + [periods.DateUnit.MONTH, periods.Instant((2012, 2, 3)), 1, 1], + [periods.DateUnit.MONTH, periods.Instant((2022, 1, 3)), 3, 3], + [periods.DateUnit.MONTH, periods.Instant((2012, 1, 3)), 3, 3], + [periods.DateUnit.YEAR, periods.Instant((2022, 12, 1)), 1, 12], + [periods.DateUnit.YEAR, periods.Instant((2012, 1, 1)), 1, 12], + [periods.DateUnit.YEAR, periods.Instant((2022, 1, 1)), 2, 24], + ]) +def test_day_size_in_months(date_unit, instant, size, expected): + """Returns the expected number of months.""" + + period = periods.Period((date_unit, instant, size)) + + assert period.size_in_months == expected + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.DateUnit.DAY, periods.Instant((2022, 12, 31)), 1, 1], + [periods.DateUnit.DAY, periods.Instant((2022, 12, 31)), 3, 3], + [periods.DateUnit.MONTH, periods.Instant((2012, 1, 3)), 3, 31 + 29 + 31], + [periods.DateUnit.MONTH, periods.Instant((2012, 2, 3)), 1, 29], + [periods.DateUnit.MONTH, periods.Instant((2022, 1, 3)), 3, 31 + 28 + 31], + [periods.DateUnit.MONTH, periods.Instant((2022, 12, 1)), 1, 31], + [periods.DateUnit.YEAR, periods.Instant((2012, 1, 1)), 1, 366], + [periods.DateUnit.YEAR, periods.Instant((2022, 1, 1)), 2, 730], + [periods.DateUnit.YEAR, periods.Instant((2022, 12, 1)), 1, 365], + ]) +def test_day_size_in_days(date_unit, instant, size, expected): + """Returns the expected number of days.""" + + period = periods.Period((date_unit, instant, size)) + + assert period.size_in_days == expected + + +@pytest.mark.parametrize("period_unit, unit, start, cease, count", [ + [periods.DateUnit.YEAR, periods.DateUnit.YEAR, periods.Instant((2022, 1, 1)), periods.Instant((2024, 1, 1)), 3], + [periods.DateUnit.YEAR, periods.DateUnit.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2025, 11, 1)), 36], + [periods.DateUnit.YEAR, periods.DateUnit.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2025, 12, 30)), 1096], + [periods.DateUnit.MONTH, periods.DateUnit.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2023, 2, 1)), 3], + [periods.DateUnit.MONTH, periods.DateUnit.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2023, 3, 30)), 90], + [periods.DateUnit.DAY, periods.DateUnit.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2023, 1, 2)), 3], + ]) +def test_subperiods(instant, period_unit, unit, start, cease, count): + """Returns the expected subperiods.""" + + period = periods.Period((period_unit, instant, 3)) + subperiods = period.subperiods(unit) + + assert len(subperiods) == count + assert subperiods[0] == periods.Period((unit, start, 1)) + assert subperiods[-1] == periods.Period((unit, cease, 1)) + + +@pytest.mark.parametrize("period_unit, offset, unit, expected", [ + [periods.DateUnit.YEAR, "first-of", periods.DateUnit.YEAR, periods.Period(('year', periods.Instant((2022, 1, 1)), 3))], + [periods.DateUnit.YEAR, "first-of", periods.DateUnit.MONTH, periods.Period(('year', periods.Instant((2022, 12, 1)), 3))], + [periods.DateUnit.YEAR, "last-of", periods.DateUnit.YEAR, periods.Period(('year', periods.Instant((2022, 12, 31)), 3))], + [periods.DateUnit.YEAR, "last-of", periods.DateUnit.MONTH, periods.Period(('year', periods.Instant((2022, 12, 31)), 3))], + [periods.DateUnit.YEAR, -3, periods.DateUnit.YEAR, periods.Period(('year', periods.Instant((2019, 12, 31)), 3))], + [periods.DateUnit.YEAR, 1, periods.DateUnit.MONTH, periods.Period(('year', periods.Instant((2023, 1, 31)), 3))], + [periods.DateUnit.YEAR, 3, periods.DateUnit.DAY, periods.Period(('year', periods.Instant((2023, 1, 3)), 3))], + [periods.DateUnit.MONTH, "first-of", periods.DateUnit.YEAR, periods.Period(('month', periods.Instant((2022, 1, 1)), 3))], + [periods.DateUnit.MONTH, "first-of", periods.DateUnit.MONTH, periods.Period(('month', periods.Instant((2022, 12, 1)), 3))], + [periods.DateUnit.MONTH, "last-of", periods.DateUnit.YEAR, periods.Period(('month', periods.Instant((2022, 12, 31)), 3))], + [periods.DateUnit.MONTH, "last-of", periods.DateUnit.MONTH, periods.Period(('month', periods.Instant((2022, 12, 31)), 3))], + [periods.DateUnit.MONTH, -3, periods.DateUnit.YEAR, periods.Period(('month', periods.Instant((2019, 12, 31)), 3))], + [periods.DateUnit.MONTH, 1, periods.DateUnit.MONTH, periods.Period(('month', periods.Instant((2023, 1, 31)), 3))], + [periods.DateUnit.MONTH, 3, periods.DateUnit.DAY, periods.Period(('month', periods.Instant((2023, 1, 3)), 3))], + [periods.DateUnit.DAY, "first-of", periods.DateUnit.YEAR, periods.Period(('day', periods.Instant((2022, 1, 1)), 3))], + [periods.DateUnit.DAY, "first-of", periods.DateUnit.MONTH, periods.Period(('day', periods.Instant((2022, 12, 1)), 3))], + [periods.DateUnit.DAY, "last-of", periods.DateUnit.YEAR, periods.Period(('day', periods.Instant((2022, 12, 31)), 3))], + [periods.DateUnit.DAY, "last-of", periods.DateUnit.MONTH, periods.Period(('day', periods.Instant((2022, 12, 31)), 3))], + [periods.DateUnit.DAY, -3, periods.DateUnit.YEAR, periods.Period(('day', periods.Instant((2019, 12, 31)), 3))], + [periods.DateUnit.DAY, 1, periods.DateUnit.MONTH, periods.Period(('day', periods.Instant((2023, 1, 31)), 3))], + [periods.DateUnit.DAY, 3, periods.DateUnit.DAY, periods.Period(('day', periods.Instant((2023, 1, 3)), 3))], + ]) +def test_offset(instant, period_unit, offset, unit, expected): + """Returns the expected ``Period``.""" + + period = periods.Period((period_unit, instant, 3)) + + assert period.offset(offset, unit) == expected diff --git a/openfisca_core/periods/tests/test_utils.py b/openfisca_core/periods/tests/test_utils.py new file mode 100644 index 0000000000..8e340a82c9 --- /dev/null +++ b/openfisca_core/periods/tests/test_utils.py @@ -0,0 +1,17 @@ +import pytest + +from openfisca_core import periods + + +@pytest.mark.parametrize("arg, expected", [ + [periods.Period((periods.DateUnit.DAY, periods.Instant((1, 1, 1)), 365)), "100_365"], + [periods.Period((periods.DateUnit.MONTH, periods.Instant((1, 1, 1)), 12)), "200_12"], + [periods.Period((periods.DateUnit.YEAR, periods.Instant((1, 1, 1)), 2)), "300_2"], + [periods.Period((periods.DateUnit.ETERNITY, periods.Instant((1, 1, 1)), 1)), "400_1"], + [(periods.DateUnit.DAY, None, 1), "100_1"], + [(periods.DateUnit.MONTH, None, -1000), "200_-1000"], + ]) +def test_key_period_size(arg, expected): + """Returns the corresponding period's weight.""" + + assert periods.key_period_size(arg) == expected diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py new file mode 100644 index 0000000000..93e0afe381 --- /dev/null +++ b/openfisca_core/periods/typing.py @@ -0,0 +1,40 @@ +# pylint: disable=missing-class-docstring,missing-function-docstring + +from __future__ import annotations + +import typing_extensions +from typing import Any +from typing_extensions import Protocol + +import abc + + +@typing_extensions.runtime_checkable +class Instant(Protocol): + @property + @abc.abstractmethod + def date(self) -> Any: ... + + @abc.abstractmethod + def offset(self, offset: Any, unit: Any) -> Any: ... + + +@typing_extensions.runtime_checkable +class Period(Protocol): + @abc.abstractmethod + def __iter__(self) -> Any: ... + + @property + @abc.abstractmethod + def unit(self) -> Any: ... + + @property + @abc.abstractmethod + def start(self) -> Any: ... + + @property + @abc.abstractmethod + def stop(self) -> Any: ... + + @abc.abstractmethod + def offset(self, offset: Any, unit: Any = None) -> Any: ... diff --git a/openfisca_core/populations/__init__.py b/openfisca_core/populations/__init__.py index 7dedd71dc6..f5d26720be 100644 --- a/openfisca_core/populations/__init__.py +++ b/openfisca_core/populations/__init__.py @@ -22,15 +22,14 @@ # See: https://www.python.org/dev/peps/pep-0008/#imports from openfisca_core.projectors import ( # noqa: F401 - Projector, EntityToPersonProjector, FirstPersonToEntityProjector, + Projector, UniqueRoleToEntityProjector, ) - from openfisca_core.projectors.helpers import ( # noqa: F401 - projectable, get_projector_from_shortcut, + projectable, ) from .config import ADD, DIVIDE # noqa: F401 diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index cb243aff70..9a6c66c9f2 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -1,5 +1,7 @@ from __future__ import annotations +from openfisca_core.periods.typing import Period +from openfisca_core.types import Array, Entity, Role, Simulation from typing import Dict, NamedTuple, Optional, Sequence, Union from typing_extensions import TypedDict @@ -10,7 +12,6 @@ from openfisca_core import periods, projectors from openfisca_core.holders import Holder, MemoryUsage from openfisca_core.projectors import Projector -from openfisca_core.types import Array, Entity, Period, Role, Simulation from . import config @@ -111,7 +112,7 @@ def __call__( calculate: Calculate = Calculate( variable = variable_name, - period = periods.period(period), + period = periods.build_period(period), option = options, ) diff --git a/openfisca_core/projectors/__init__.py b/openfisca_core/projectors/__init__.py index 02982bf982..44697546ee 100644 --- a/openfisca_core/projectors/__init__.py +++ b/openfisca_core/projectors/__init__.py @@ -21,8 +21,8 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .helpers import projectable, get_projector_from_shortcut # noqa: F401 from .projector import Projector # noqa: F401 from .entity_to_person_projector import EntityToPersonProjector # noqa: F401 from .first_person_to_entity_projector import FirstPersonToEntityProjector # noqa: F401 +from .helpers import get_projector_from_shortcut, projectable # noqa: F401 from .unique_role_to_entity_projector import UniqueRoleToEntityProjector # noqa: F401 diff --git a/openfisca_core/scripts/__init__.py b/openfisca_core/scripts/__init__.py index 9e0a3b67bc..4ee62e6572 100644 --- a/openfisca_core/scripts/__init__.py +++ b/openfisca_core/scripts/__init__.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -import traceback import importlib import logging import pkgutil +import traceback from os import linesep log = logging.getLogger(__name__) diff --git a/openfisca_core/scripts/find_placeholders.py b/openfisca_core/scripts/find_placeholders.py index b14fd5fea9..53802b5467 100644 --- a/openfisca_core/scripts/find_placeholders.py +++ b/openfisca_core/scripts/find_placeholders.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- # flake8: noqa T001 -import os import fnmatch +import os import sys from bs4 import BeautifulSoup diff --git a/openfisca_core/scripts/measure_numpy_condition_notations.py b/openfisca_core/scripts/measure_numpy_condition_notations.py index 371de722a1..bbb67011e4 100755 --- a/openfisca_core/scripts/measure_numpy_condition_notations.py +++ b/openfisca_core/scripts/measure_numpy_condition_notations.py @@ -11,14 +11,13 @@ The aim of this script is to compare the time taken by the calculation of the values """ -from contextlib import contextmanager import argparse import sys import time +from contextlib import contextmanager import numpy - args = None diff --git a/openfisca_core/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index 5c4fced850..8eabd54910 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -13,12 +13,11 @@ from numpy.core.defchararray import startswith from openfisca_core import periods, simulations -from openfisca_core.periods import ETERNITY from openfisca_core.entities import build_entity -from openfisca_core.variables import Variable +from openfisca_core.periods import DateUnit from openfisca_core.taxbenefitsystems import TaxBenefitSystem from openfisca_core.tools import assert_near - +from openfisca_core.variables import Variable args = None @@ -82,7 +81,7 @@ class city_code(Variable): value_type = 'FixedStr' max_length = 5 entity = Famille - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY label = """Code INSEE "city_code" de la commune de résidence de la famille""" @@ -106,7 +105,7 @@ def formula(self, simulation, period): if age_en_mois is not None: return age_en_mois // 12 birth = simulation.calculate('birth', period) - return (numpy.datetime64(period.date) - birth).astype('timedelta64[Y]') + return (numpy.datetime64(period.date()) - birth).astype('timedelta64[Y]') class dom_tom(Variable): @@ -115,7 +114,7 @@ class dom_tom(Variable): label = "La famille habite-t-elle les DOM-TOM ?" def formula(self, simulation, period): - period = period.start.period('year').offset('first-of') + period = period.start.period(periods.DateUnit.YEAR).offset('first-of') city_code = simulation.calculate('city_code', period) return numpy.logical_or(startswith(city_code, '97'), startswith(city_code, '98')) @@ -126,7 +125,7 @@ class revenu_disponible(Variable): label = "Revenu disponible de l'individu" def formula(self, simulation, period): - period = period.start.period('year').offset('first-of') + period = period.start.period(periods.DateUnit.YEAR).offset('first-of') rsa = simulation.calculate('rsa', period) salaire_imposable = simulation.calculate('salaire_imposable', period) return rsa + salaire_imposable * 0.7 @@ -138,17 +137,17 @@ class rsa(Variable): label = "RSA" def formula_2010_01_01(self, simulation, period): - period = period.start.period('month').offset('first-of') + period = period.start.period(periods.DateUnit.MONTH).offset('first-of') salaire_imposable = simulation.calculate('salaire_imposable', period) return (salaire_imposable < 500) * 100.0 def formula_2011_01_01(self, simulation, period): - period = period.start.period('month').offset('first-of') + period = period.start.period(periods.DateUnit.MONTH).offset('first-of') salaire_imposable = simulation.calculate('salaire_imposable', period) return (salaire_imposable < 500) * 200.0 def formula_2013_01_01(self, simulation, period): - period = period.start.period('month').offset('first-of') + period = period.start.period(periods.DateUnit.MONTH).offset('first-of') salaire_imposable = simulation.calculate('salaire_imposable', period) return (salaire_imposable < 500) * 300 @@ -159,7 +158,7 @@ class salaire_imposable(Variable): label = "Salaire imposable" def formula(individu, period): - period = period.start.period('year').offset('first-of') + period = period.start.period(periods.DateUnit.YEAR).offset('first-of') dom_tom = individu.famille('dom_tom', period) salaire_net = individu('salaire_net', period) return salaire_net * 0.9 - 100 * dom_tom @@ -171,7 +170,7 @@ class salaire_net(Variable): label = "Salaire net" def formula(self, simulation, period): - period = period.start.period('year').offset('first-of') + period = period.start.period(periods.DateUnit.YEAR).offset('first-of') salaire_brut = simulation.calculate('salaire_brut', period) return salaire_brut * 0.8 @@ -186,7 +185,7 @@ def formula(self, simulation, period): @timeit def check_revenu_disponible(year, city_code, expected_revenu_disponible): - simulation = simulations.Simulation(period = periods.period(year), tax_benefit_system = tax_benefit_system) + simulation = simulations.Simulation(period = periods.build_period(year), tax_benefit_system = tax_benefit_system) famille = simulation.populations["famille"] famille.count = 3 famille.roles_count = 2 diff --git a/openfisca_core/scripts/measure_performances_fancy_indexing.py b/openfisca_core/scripts/measure_performances_fancy_indexing.py index 82cf20f0c0..a13e950ddb 100644 --- a/openfisca_core/scripts/measure_performances_fancy_indexing.py +++ b/openfisca_core/scripts/measure_performances_fancy_indexing.py @@ -1,10 +1,9 @@ # flake8: noqa T001 -import numpy import timeit -from openfisca_france import CountryTaxBenefitSystem -from openfisca_core.model_api import * # noqa analysis:ignore +import numpy +from openfisca_france import CountryTaxBenefitSystem tbs = CountryTaxBenefitSystem() N = 200000 @@ -12,14 +11,14 @@ zone_apl = numpy.random.choice([1, 2, 3], N) al_nb_pac = numpy.random.choice(6, N) couple = numpy.random.choice([True, False], N) -formatted_zone = concat('plafond_pour_accession_a_la_propriete_zone_', zone_apl) # zone_apl returns 1, 2 or 3 but the parameters have a long name +formatted_zone = numpy.concat('plafond_pour_accession_a_la_propriete_zone_', zone_apl) # zone_apl returns 1, 2 or 3 but the parameters have a long name def formula_with(): plafonds = al_plaf_acc[formatted_zone] result = ( - plafonds.personne_isolee_sans_enfant * not_(couple) * (al_nb_pac == 0) + plafonds.personne_isolee_sans_enfant * numpy.logical_not(couple) * (al_nb_pac == 0) + plafonds.menage_seul * couple * (al_nb_pac == 0) + plafonds.menage_ou_isole_avec_1_enfant * (al_nb_pac == 1) + plafonds.menage_ou_isole_avec_2_enfants * (al_nb_pac == 2) @@ -38,7 +37,7 @@ def formula_without(): z3 = al_plaf_acc.plafond_pour_accession_a_la_propriete_zone_3 return (zone_apl == 1) * ( - z1.personne_isolee_sans_enfant * not_(couple) * (al_nb_pac == 0) + z1.personne_isolee_sans_enfant * numpy.logical_not(couple) * (al_nb_pac == 0) + z1.menage_seul * couple * (al_nb_pac == 0) + z1.menage_ou_isole_avec_1_enfant * (al_nb_pac == 1) + z1.menage_ou_isole_avec_2_enfants * (al_nb_pac == 2) @@ -47,7 +46,7 @@ def formula_without(): + z1.menage_ou_isole_avec_5_enfants * (al_nb_pac >= 5) + z1.menage_ou_isole_par_enfant_en_plus * (al_nb_pac > 5) * (al_nb_pac - 5) ) + (zone_apl == 2) * ( - z2.personne_isolee_sans_enfant * not_(couple) * (al_nb_pac == 0) + z2.personne_isolee_sans_enfant * numpy.logical_not(couple) * (al_nb_pac == 0) + z2.menage_seul * couple * (al_nb_pac == 0) + z2.menage_ou_isole_avec_1_enfant * (al_nb_pac == 1) + z2.menage_ou_isole_avec_2_enfants * (al_nb_pac == 2) @@ -56,7 +55,7 @@ def formula_without(): + z2.menage_ou_isole_avec_5_enfants * (al_nb_pac >= 5) + z2.menage_ou_isole_par_enfant_en_plus * (al_nb_pac > 5) * (al_nb_pac - 5) ) + (zone_apl == 3) * ( - z3.personne_isolee_sans_enfant * not_(couple) * (al_nb_pac == 0) + z3.personne_isolee_sans_enfant * numpy.logical_not(couple) * (al_nb_pac == 0) + z3.menage_seul * couple * (al_nb_pac == 0) + z3.menage_ou_isole_avec_1_enfant * (al_nb_pac == 1) + z3.menage_ou_isole_avec_2_enfants * (al_nb_pac == 2) diff --git a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py index 6e8f672988..474c2fbb86 100644 --- a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py +++ b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_country_template.py @@ -7,10 +7,11 @@ or just (output is written in a directory called `yaml_parameters`): `python xml_to_yaml_country_template.py` ''' -import sys import os +import sys + +from openfisca_country_template import COUNTRY_DIR, CountryTaxBenefitSystem -from openfisca_country_template import CountryTaxBenefitSystem, COUNTRY_DIR from . import xml_to_yaml tax_benefit_system = CountryTaxBenefitSystem() diff --git a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py index 91144ed6a0..f4c551f930 100644 --- a/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py +++ b/openfisca_core/scripts/migrations/v16_2_to_v17/xml_to_yaml_extension_template.py @@ -8,12 +8,13 @@ `python xml_to_yaml_extension_template.py` ''' -import sys import os +import sys -from . import xml_to_yaml import openfisca_extension_template +from . import xml_to_yaml + if len(sys.argv) > 1: target_path = sys.argv[1] else: diff --git a/openfisca_core/scripts/migrations/v24_to_25.py b/openfisca_core/scripts/migrations/v24_to_25.py index 853c4e9a94..2c065aa456 100644 --- a/openfisca_core/scripts/migrations/v24_to_25.py +++ b/openfisca_core/scripts/migrations/v24_to_25.py @@ -2,14 +2,17 @@ # flake8: noqa T001 import argparse -import os import glob +import os +from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedSeq -from openfisca_core.scripts import add_tax_benefit_system_arguments, build_tax_benefit_system +from openfisca_core.scripts import ( + add_tax_benefit_system_arguments, + build_tax_benefit_system, + ) -from ruamel.yaml import YAML yaml = YAML() yaml.default_flow_style = False yaml.width = 4096 diff --git a/openfisca_core/scripts/openfisca_command.py b/openfisca_core/scripts/openfisca_command.py index 3689771c63..ea343f2ea3 100644 --- a/openfisca_core/scripts/openfisca_command.py +++ b/openfisca_core/scripts/openfisca_command.py @@ -1,8 +1,9 @@ import argparse -import warnings import sys +import warnings from openfisca_core.scripts import add_tax_benefit_system_arguments + """ Define the `openfisca` command line interface. """ diff --git a/openfisca_core/scripts/remove_fuzzy.py b/openfisca_core/scripts/remove_fuzzy.py index a4417d9df0..279d69013e 100755 --- a/openfisca_core/scripts/remove_fuzzy.py +++ b/openfisca_core/scripts/remove_fuzzy.py @@ -1,9 +1,10 @@ # remove_fuzzy.py : Remove the fuzzy attribute in xml files and add END tags. # See https://github.com/openfisca/openfisca-core/issues/437 -import re import datetime +import re import sys + import numpy assert(len(sys.argv) == 2) diff --git a/openfisca_core/scripts/run_test.py b/openfisca_core/scripts/run_test.py index 72f05bc90c..add0842fcf 100644 --- a/openfisca_core/scripts/run_test.py +++ b/openfisca_core/scripts/run_test.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- import logging -import sys import os +import sys -from openfisca_core.tools.test_runner import run_tests from openfisca_core.scripts import build_tax_benefit_system +from openfisca_core.tools.test_runner import run_tests def main(parser): diff --git a/openfisca_core/scripts/simulation_generator.py b/openfisca_core/scripts/simulation_generator.py index c100c85a2c..7bc82b8386 100644 --- a/openfisca_core/scripts/simulation_generator.py +++ b/openfisca_core/scripts/simulation_generator.py @@ -1,6 +1,7 @@ +import random + import numpy -import random from openfisca_core.simulations import Simulation diff --git a/openfisca_core/simulation_builder.py b/openfisca_core/simulation_builder.py index 57c7765ebe..4b24e06838 100644 --- a/openfisca_core/simulation_builder.py +++ b/openfisca_core/simulation_builder.py @@ -6,11 +6,11 @@ # The following are transitional imports to ensure non-breaking changes. # Could be deprecated in the next major release. -from openfisca_core.simulations import ( # noqa: F401 - Simulation, - SimulationBuilder, +from openfisca_core.simulations import ( # noqa: F401 calculate_output_add, calculate_output_divide, check_type, + Simulation, + SimulationBuilder, transform_to_strict_syntax, ) diff --git a/openfisca_core/simulations/__init__.py b/openfisca_core/simulations/__init__.py index 5b02dc1a22..8b655aac71 100644 --- a/openfisca_core/simulations/__init__.py +++ b/openfisca_core/simulations/__init__.py @@ -21,8 +21,17 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from openfisca_core.errors import CycleError, NaNCreationError, SpiralError # noqa: F401 +from openfisca_core.errors import ( # noqa: F401 + CycleError, + NaNCreationError, + SpiralError, + ) -from .helpers import calculate_output_add, calculate_output_divide, check_type, transform_to_strict_syntax # noqa: F401 +from .helpers import ( # noqa: F401 + calculate_output_add, + calculate_output_divide, + check_type, + transform_to_strict_syntax, + ) from .simulation import Simulation # noqa: F401 from .simulation_builder import SimulationBuilder # noqa: F401 diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index ef1cbbd869..56f02da5bf 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,5 +1,6 @@ from __future__ import annotations +from openfisca_core.types import Population, TaxBenefitSystem, Variable from typing import Dict, NamedTuple, Optional, Set import tempfile @@ -8,11 +9,18 @@ import numpy from openfisca_core import commons, periods -from openfisca_core.errors import CycleError, SpiralError, VariableNotFoundError +from openfisca_core.errors import ( + CycleError, + SpiralError, + VariableNotFoundError, + ) from openfisca_core.indexed_enums import Enum, EnumArray -from openfisca_core.periods import Period -from openfisca_core.tracers import FullTracer, SimpleTracer, TracingParameterNodeAtInstant -from openfisca_core.types import Population, TaxBenefitSystem, Variable +from openfisca_core.periods import DateUnit, Period +from openfisca_core.tracers import ( + FullTracer, + SimpleTracer, + TracingParameterNodeAtInstant, + ) from openfisca_core.warnings import TempfileWarning @@ -96,7 +104,7 @@ def calculate(self, variable_name: str, period): """Calculate ``variable_name`` for ``period``.""" if period is not None and not isinstance(period, Period): - period = periods.period(period) + period = periods.build_period(period) self.tracer.record_calculation_start(variable_name, period) @@ -168,24 +176,24 @@ def calculate_add(self, variable_name: str, period): raise VariableNotFoundError(variable_name, self.tax_benefit_system) if period is not None and not isinstance(period, Period): - period = periods.period(period) + period = periods.build_period(period) # Check that the requested period matches definition_period - if periods.unit_weight(variable.definition_period) > periods.unit_weight(period.unit): + if periods.UNIT_WEIGHTS[variable.definition_period] > periods.UNIT_WEIGHTS[period.unit]: raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' can only be computed for {2}-long periods. You can use the DIVIDE option to get an estimate of {0} by dividing the yearly value by 12, or change the requested period to 'period.this_year'.".format( variable.name, period, variable.definition_period )) - if variable.definition_period not in [periods.DAY, periods.MONTH, periods.YEAR]: + if variable.definition_period not in [DateUnit.DAY, periods.DateUnit.MONTH, periods.DateUnit.YEAR]: raise ValueError("Unable to sum constant variable '{}' over period {}: only variables defined daily, monthly, or yearly can be summed over time.".format( variable.name, period)) return sum( self.calculate(variable_name, sub_period) - for sub_period in period.get_subperiods(variable.definition_period) + for sub_period in period.subperiods(variable.definition_period) ) def calculate_divide(self, variable_name: str, period): @@ -197,10 +205,10 @@ def calculate_divide(self, variable_name: str, period): raise VariableNotFoundError(variable_name, self.tax_benefit_system) if period is not None and not isinstance(period, Period): - period = periods.period(period) + period = periods.build_period(period) # Check that the requested period matches definition_period - if variable.definition_period != periods.YEAR: + if variable.definition_period != periods.DateUnit.YEAR: raise ValueError("Unable to divide the value of '{}' over time on period {}: only variables defined yearly can be divided over time.".format( variable_name, period)) @@ -208,10 +216,10 @@ def calculate_divide(self, variable_name: str, period): if period.size != 1: raise ValueError("DIVIDE option can only be used for a one-year or a one-month requested period") - if period.unit == periods.MONTH: + if period.unit == periods.DateUnit.MONTH: computation_period = period.this_year return self.calculate(variable_name, period = computation_period) / 12. - elif period.unit == periods.YEAR: + elif period.unit == periods.DateUnit.YEAR: return self.calculate(variable_name, period) raise ValueError("Unable to divide the value of '{}' to match period {}.".format( @@ -266,16 +274,16 @@ def _check_period_consistency(self, period, variable): """ Check that a period matches the variable definition_period """ - if variable.definition_period == periods.ETERNITY: + if variable.definition_period == periods.DateUnit.ETERNITY: return # For variables which values are constant in time, all periods are accepted - if variable.definition_period == periods.MONTH and period.unit != periods.MONTH: + if variable.definition_period == periods.DateUnit.MONTH and period.unit != periods.DateUnit.MONTH: raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole month. You can use the ADD option to sum '{0}' over the requested period, or change the requested period to 'period.first_month'.".format( variable.name, period )) - if variable.definition_period == periods.YEAR and period.unit != periods.YEAR: + if variable.definition_period == periods.DateUnit.YEAR and period.unit != periods.DateUnit.YEAR: raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole year. You can use the DIVIDE option to get an estimate of {0} by dividing the yearly value by 12, or change the requested period to 'period.this_year'.".format( variable.name, period @@ -285,7 +293,7 @@ def _check_period_consistency(self, period, variable): raise ValueError("Unable to compute variable '{0}' for period {1}: '{0}' must be computed for a whole {2}. You can use the ADD option to sum '{0}' over the requested period.".format( variable.name, period, - 'month' if variable.definition_period == periods.MONTH else 'year' + periods.DateUnit.MONTH if variable.definition_period == periods.DateUnit.MONTH else periods.DateUnit.YEAR )) def _cast_formula_result(self, value, variable): @@ -345,7 +353,7 @@ def get_array(self, variable_name: str, period): Unlike :meth:`.calculate`, this method *does not* trigger calculations and *does not* use any formula. """ if period is not None and not isinstance(period, Period): - period = periods.period(period) + period = periods.build_period(period) return self.get_holder(variable_name).get_array(period) def get_holder(self, variable_name: str): @@ -438,8 +446,8 @@ def set_input(self, variable_name: str, period, value): if variable is None: raise VariableNotFoundError(variable_name, self.tax_benefit_system) - period = periods.period(period) - if ((variable.end is not None) and (period.start.date > variable.end)): + period = periods.build_period(period) + if ((variable.end is not None) and (period.start.date() > variable.end)): return self.get_holder(variable_name).set_input(period, value) diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 0092ba8371..6dbde1ec8e 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -1,13 +1,17 @@ -from typing import Dict, List, Iterable +from typing import Dict, Iterable, List import copy -import dpath.util +import dpath.util import numpy from openfisca_core import periods from openfisca_core.entities import Entity -from openfisca_core.errors import PeriodMismatchError, SituationParsingError, VariableNotFoundError +from openfisca_core.errors import ( + PeriodMismatchError, + SituationParsingError, + VariableNotFoundError, + ) from openfisca_core.populations import Population from openfisca_core.simulations import helpers, Simulation from openfisca_core.variables import Variable @@ -325,7 +329,7 @@ def add_group_entity(self, persons_plural, persons_ids, entity, instances_json): def set_default_period(self, period_str): if period_str: - self.default_period = str(periods.period(period_str)) + self.default_period = str(periods.build_period(period_str)) def get_input(self, variable, period_str): if variable not in self.input_buffer: @@ -368,7 +372,7 @@ def init_variable_values(self, entity, instance_object, instance_id): for period_str, value in variable_values.items(): try: - periods.period(period_str) + periods.build_period(period_str) except ValueError as e: raise SituationParsingError(path_in_json, e.args[0]) variable = entity.get_variable(variable_name) @@ -393,7 +397,7 @@ def add_variable_value(self, entity, variable, instance_index, instance_id, peri array[instance_index] = value - self.input_buffer[variable.name][str(periods.period(period_str))] = array + self.input_buffer[variable.name][str(periods.build_period(period_str))] = array def finalize_variables_init(self, population): # Due to set_input mechanism, we must bufferize all inputs, then actually set them, @@ -411,7 +415,7 @@ def finalize_variables_init(self, population): except ValueError: # Wrong entity, we can just ignore that continue buffer = self.input_buffer[variable_name] - unsorted_periods = [periods.period(period_str) for period_str in self.input_buffer[variable_name].keys()] + unsorted_periods = [periods.build_period(period_str) for period_str in self.input_buffer[variable_name].keys()] # We need to handle small periods first for set_input to work sorted_periods = sorted(unsorted_periods, key = periods.key_period_size) for period_value in sorted_periods: @@ -422,7 +426,7 @@ def finalize_variables_init(self, population): variable = holder.variable # TODO - this duplicates the check in Simulation.set_input, but # fixing that requires improving Simulation's handling of entities - if (variable.end is None) or (period_value.start.date <= variable.end): + if (variable.end is None) or (period_value.start.date() <= variable.end): holder.set_input(period_value, array) def raise_period_mismatch(self, entity, json, e): diff --git a/openfisca_core/taxbenefitsystems/__init__.py b/openfisca_core/taxbenefitsystems/__init__.py index bf5f224c2c..e39b1faec6 100644 --- a/openfisca_core/taxbenefitsystems/__init__.py +++ b/openfisca_core/taxbenefitsystems/__init__.py @@ -21,6 +21,9 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from openfisca_core.errors import VariableNameConflict, VariableNotFound # noqa: F401 +from openfisca_core.errors import ( # noqa: F401 + VariableNameConflict, + VariableNotFound, + ) from .tax_benefit_system import TaxBenefitSystem # noqa: F401 diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 9a8831269d..421b3dc520 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -1,27 +1,30 @@ from __future__ import annotations +import typing from typing import Any, Dict, Optional, Sequence, Union import copy import functools import glob import importlib -import importlib_metadata import inspect import logging import os import sys import traceback -import typing -from openfisca_core import commons, periods, variables +import importlib_metadata + +from openfisca_core import commons, periods, types, variables from openfisca_core.entities import Entity -from openfisca_core.errors import VariableNameConflictError, VariableNotFoundError +from openfisca_core.errors import ( + VariableNameConflictError, + VariableNotFoundError, + ) from openfisca_core.parameters import ParameterNode from openfisca_core.periods import Instant, Period -from openfisca_core.populations import Population, GroupPopulation +from openfisca_core.populations import GroupPopulation, Population from openfisca_core.simulations import SimulationBuilder -from openfisca_core.types import ParameterNodeAtInstant from openfisca_core.variables import Variable log = logging.getLogger(__name__) @@ -43,7 +46,7 @@ class TaxBenefitSystem: person_entity: Entity _base_tax_benefit_system = None - _parameters_at_instant_cache: Dict[Instant, ParameterNodeAtInstant] = {} + _parameters_at_instant_cache: Dict[Instant, types.ParameterNodeAtInstant] = {} person_key_plural = None preprocess_parameters = None baseline = None # Baseline tax-benefit system. Used only by reforms. Note: Reforms can be chained. @@ -386,7 +389,7 @@ def _get_baseline_parameters_at_instant(self, instant): def get_parameters_at_instant( self, instant: Union[str, int, Period, Instant], - ) -> Optional[ParameterNodeAtInstant]: + ) -> Optional[types.ParameterNodeAtInstant]: """Get the parameters of the legislation at a given instant Args: @@ -397,7 +400,7 @@ def get_parameters_at_instant( """ - key: Instant + key: Optional[Instant] msg: str if isinstance(instant, Instant): @@ -407,10 +410,10 @@ def get_parameters_at_instant( key = instant.start elif isinstance(instant, (str, int)): - key = periods.instant(instant) + key = periods.build_instant(instant) else: - msg = f"Expected an Instant (e.g. Instant((2017, 1, 1)) ). Got: {key}." + msg = f"Expected an Instant (e.g. Instant((2017, 1, 1)) ). Got: {instant}." raise AssertionError(msg) if self.parameters is None: diff --git a/openfisca_core/taxscales/__init__.py b/openfisca_core/taxscales/__init__.py index 0364101d71..b6f248f159 100644 --- a/openfisca_core/taxscales/__init__.py +++ b/openfisca_core/taxscales/__init__.py @@ -23,13 +23,13 @@ from openfisca_core.errors import EmptyArgumentError # noqa: F401 -from .helpers import combine_tax_scales # noqa: F401 from .tax_scale_like import TaxScaleLike # noqa: F401 from .rate_tax_scale_like import RateTaxScaleLike # noqa: F401 -from .marginal_rate_tax_scale import MarginalRateTaxScale # noqa: F401 -from .linear_average_rate_tax_scale import LinearAverageRateTaxScale # noqa: F401 +from .abstract_rate_tax_scale import AbstractRateTaxScale # noqa: F401 from .abstract_tax_scale import AbstractTaxScale # noqa: F401 from .amount_tax_scale_like import AmountTaxScaleLike # noqa: F401 -from .abstract_rate_tax_scale import AbstractRateTaxScale # noqa: F401 +from .helpers import combine_tax_scales # noqa: F401 +from .linear_average_rate_tax_scale import LinearAverageRateTaxScale # noqa: F401 from .marginal_amount_tax_scale import MarginalAmountTaxScale # noqa: F401 +from .marginal_rate_tax_scale import MarginalRateTaxScale # noqa: F401 from .single_amount_tax_scale import SingleAmountTaxScale # noqa: F401 diff --git a/openfisca_core/taxscales/abstract_rate_tax_scale.py b/openfisca_core/taxscales/abstract_rate_tax_scale.py index b9316273d1..d1337ca0fd 100644 --- a/openfisca_core/taxscales/abstract_rate_tax_scale.py +++ b/openfisca_core/taxscales/abstract_rate_tax_scale.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing + import warnings from openfisca_core.taxscales import RateTaxScaleLike diff --git a/openfisca_core/taxscales/abstract_tax_scale.py b/openfisca_core/taxscales/abstract_tax_scale.py index 9cbeeb7565..acd801ab76 100644 --- a/openfisca_core/taxscales/abstract_tax_scale.py +++ b/openfisca_core/taxscales/abstract_tax_scale.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing + import warnings from openfisca_core.taxscales import TaxScaleLike diff --git a/openfisca_core/taxscales/amount_tax_scale_like.py b/openfisca_core/taxscales/amount_tax_scale_like.py index cfc0a6973f..c134e7f52c 100644 --- a/openfisca_core/taxscales/amount_tax_scale_like.py +++ b/openfisca_core/taxscales/amount_tax_scale_like.py @@ -1,7 +1,8 @@ +import typing + import abc import bisect import os -import typing from openfisca_core import tools from openfisca_core.taxscales import TaxScaleLike diff --git a/openfisca_core/taxscales/helpers.py b/openfisca_core/taxscales/helpers.py index 181fbfed36..b9568090b9 100644 --- a/openfisca_core/taxscales/helpers.py +++ b/openfisca_core/taxscales/helpers.py @@ -1,8 +1,9 @@ from __future__ import annotations -import logging import typing +import logging + from openfisca_core import taxscales log = logging.getLogger(__name__) diff --git a/openfisca_core/taxscales/linear_average_rate_tax_scale.py b/openfisca_core/taxscales/linear_average_rate_tax_scale.py index d1fe9c8094..a196e4f4c2 100644 --- a/openfisca_core/taxscales/linear_average_rate_tax_scale.py +++ b/openfisca_core/taxscales/linear_average_rate_tax_scale.py @@ -1,8 +1,9 @@ from __future__ import annotations -import logging import typing +import logging + import numpy from openfisca_core import taxscales diff --git a/openfisca_core/taxscales/marginal_rate_tax_scale.py b/openfisca_core/taxscales/marginal_rate_tax_scale.py index 1bafdae40f..258efbc342 100644 --- a/openfisca_core/taxscales/marginal_rate_tax_scale.py +++ b/openfisca_core/taxscales/marginal_rate_tax_scale.py @@ -1,8 +1,9 @@ from __future__ import annotations +import typing + import bisect import itertools -import typing import numpy diff --git a/openfisca_core/taxscales/rate_tax_scale_like.py b/openfisca_core/taxscales/rate_tax_scale_like.py index bcb38b07e4..972195a92b 100644 --- a/openfisca_core/taxscales/rate_tax_scale_like.py +++ b/openfisca_core/taxscales/rate_tax_scale_like.py @@ -1,9 +1,10 @@ from __future__ import annotations +import typing + import abc import bisect import os -import typing import numpy diff --git a/openfisca_core/taxscales/tax_scale_like.py b/openfisca_core/taxscales/tax_scale_like.py index 8177ee0505..5723a8a92c 100644 --- a/openfisca_core/taxscales/tax_scale_like.py +++ b/openfisca_core/taxscales/tax_scale_like.py @@ -1,8 +1,9 @@ from __future__ import annotations +import typing + import abc import copy -import typing import numpy diff --git a/openfisca_core/tools/simulation_dumper.py b/openfisca_core/tools/simulation_dumper.py index 424861bba2..3879c19cdb 100644 --- a/openfisca_core/tools/simulation_dumper.py +++ b/openfisca_core/tools/simulation_dumper.py @@ -1,13 +1,10 @@ -# -*- coding: utf-8 -*- - - import os import numpy -from openfisca_core.simulations import Simulation +from openfisca_core import periods from openfisca_core.data_storage import OnDiskStorage -from openfisca_core.periods import ETERNITY +from openfisca_core.simulations import Simulation def dump_simulation(simulation, directory): @@ -116,7 +113,7 @@ def _restore_entity(population, directory): def _restore_holder(simulation, variable, directory): storage_dir = os.path.join(directory, variable) - is_variable_eternal = simulation.tax_benefit_system.get_variable(variable).definition_period == ETERNITY + is_variable_eternal = simulation.tax_benefit_system.get_variable(variable).definition_period == periods.DateUnit.ETERNITY disk_storage = OnDiskStorage( storage_dir, is_eternal = is_variable_eternal, diff --git a/openfisca_core/tools/test_runner.py b/openfisca_core/tools/test_runner.py index 4bdc238ce0..0c3bb74096 100644 --- a/openfisca_core/tools/test_runner.py +++ b/openfisca_core/tools/test_runner.py @@ -1,5 +1,6 @@ from __future__ import annotations +from openfisca_core.types import TaxBenefitSystem from typing import Any, Dict, Optional, Sequence, Union from typing_extensions import Literal, TypedDict @@ -15,7 +16,6 @@ from openfisca_core.errors import SituationParsingError, VariableNotFound from openfisca_core.simulation_builder import SimulationBuilder from openfisca_core.tools import assert_near -from openfisca_core.types import TaxBenefitSystem from openfisca_core.warnings import LibYAMLWarning diff --git a/openfisca_core/tracers/computation_log.py b/openfisca_core/tracers/computation_log.py index 9bb830b8d2..728798fd1b 100644 --- a/openfisca_core/tracers/computation_log.py +++ b/openfisca_core/tracers/computation_log.py @@ -5,9 +5,10 @@ import numpy -from .. import tracers from openfisca_core.indexed_enums import EnumArray +from .. import tracers + if typing.TYPE_CHECKING: from numpy.typing import ArrayLike diff --git a/openfisca_core/tracers/full_tracer.py b/openfisca_core/tracers/full_tracer.py index 6638a789d4..8927bc5099 100644 --- a/openfisca_core/tracers/full_tracer.py +++ b/openfisca_core/tracers/full_tracer.py @@ -1,9 +1,10 @@ from __future__ import annotations -import time import typing from typing import Dict, Iterator, List, Optional, Union +import time + from .. import tracers if typing.TYPE_CHECKING: diff --git a/openfisca_core/tracers/performance_log.py b/openfisca_core/tracers/performance_log.py index 754d7f8056..7b174daeca 100644 --- a/openfisca_core/tracers/performance_log.py +++ b/openfisca_core/tracers/performance_log.py @@ -1,11 +1,12 @@ from __future__ import annotations +import typing + import csv import importlib.resources import itertools import json import os -import typing from .. import tracers diff --git a/openfisca_core/tracers/trace_node.py b/openfisca_core/tracers/trace_node.py index 93b630886c..d9b94a30e8 100644 --- a/openfisca_core/tracers/trace_node.py +++ b/openfisca_core/tracers/trace_node.py @@ -1,8 +1,9 @@ from __future__ import annotations -import dataclasses import typing +import dataclasses + if typing.TYPE_CHECKING: import numpy diff --git a/openfisca_core/types/__init__.py b/openfisca_core/types/__init__.py index 699133aecb..12d4c3935c 100644 --- a/openfisca_core/types/__init__.py +++ b/openfisca_core/types/__init__.py @@ -11,10 +11,8 @@ * :attr:`.Entity` * :attr:`.Formula` * :attr:`.Holder` - * :attr:`.Instant` * :attr:`.ParameterNodeAtInstant` * :attr:`.Params` - * :attr:`.Period` * :attr:`.Population` * :attr:`.Role`, * :attr:`.Simulation`, @@ -49,19 +47,13 @@ # Official Public API -from ._data import ( # noqa: F401 - Array, - ArrayLike, - ) - +from ._data import Array, ArrayLike # noqa: F401 from ._domain import ( # noqa: F401 Entity, Formula, Holder, - Instant, ParameterNodeAtInstant, Params, - Period, Population, Role, Simulation, @@ -75,10 +67,8 @@ "Entity", "Formula", "Holder", - "Instant", "ParameterNodeAtInstant", "Params", - "Period", "Population", "Role", "Simulation", diff --git a/openfisca_core/types/_data.py b/openfisca_core/types/_data.py index ff7066d43a..6ee48b7a2e 100644 --- a/openfisca_core/types/_data.py +++ b/openfisca_core/types/_data.py @@ -1,7 +1,7 @@ +from nptyping import NDArray as Array +from nptyping import types from typing import Sequence, TypeVar, Union -from nptyping import types, NDArray as Array - import numpy T = TypeVar("T", bool, bytes, float, int, object, str) diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types/_domain.py index 643f27964f..12574b3b9d 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types/_domain.py @@ -1,12 +1,13 @@ from __future__ import annotations -import numpy import typing_extensions -from typing import Any, Optional +from typing import Any from typing_extensions import Protocol import abc +import numpy + class Entity(Protocol): """Entity protocol.""" @@ -26,7 +27,7 @@ def check_variable_defined_for_entity(self, variable_name: Any) -> None: def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Optional[Any]: + ) -> Any | None: """Abstract method.""" @@ -37,7 +38,7 @@ class Formula(Protocol): def __call__( self, population: Population, - instant: Instant, + instant: Any, params: Params, ) -> numpy.ndarray: """Abstract method.""" @@ -55,10 +56,6 @@ def get_memory_usage(self) -> Any: """Abstract method.""" -class Instant(Protocol): - """Instant protocol.""" - - @typing_extensions.runtime_checkable class ParameterNodeAtInstant(Protocol): """ParameterNodeAtInstant protocol.""" @@ -68,7 +65,7 @@ class Params(Protocol): """Params protocol.""" @abc.abstractmethod - def __call__(self, instant: Instant) -> ParameterNodeAtInstant: + def __call__(self, instant: Any) -> ParameterNodeAtInstant: """Abstract method.""" @@ -76,13 +73,27 @@ def __call__(self, instant: Instant) -> ParameterNodeAtInstant: class Period(Protocol): """Period protocol.""" + @abc.abstractmethod + def __iter__(self) -> Any: + """Abstract method.""" + + @property + @abc.abstractmethod + def unit(self) -> Any: + """Abstract method.""" + @property @abc.abstractmethod def start(self) -> Any: """Abstract method.""" + @property @abc.abstractmethod - def unit(self) -> Any: + def stop(self) -> Any: + """Abstract method.""" + + @abc.abstractmethod + def offset(self, offset: Any, unit: Any = None) -> Any: """Abstract method.""" @@ -119,7 +130,7 @@ def calculate_divide(self, variable_name: Any, period: Any) -> Any: """Abstract method.""" @abc.abstractmethod - def get_population(self, plural: Optional[Any]) -> Any: + def get_population(self, plural: Any | None) -> Any: """Abstract method.""" @@ -132,7 +143,7 @@ class TaxBenefitSystem(Protocol): def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Optional[Any]: + ) -> Any | None: """Abstract method.""" diff --git a/openfisca_core/variables/__init__.py b/openfisca_core/variables/__init__.py index 3decaf8f42..66db093471 100644 --- a/openfisca_core/variables/__init__.py +++ b/openfisca_core/variables/__init__.py @@ -21,6 +21,9 @@ # # See: https://www.python.org/dev/peps/pep-0008/#imports -from .config import VALUE_TYPES, FORMULA_NAME_PREFIX # noqa: F401 -from .helpers import get_annualized_variable, get_neutralized_variable # noqa: F401 +from .config import FORMULA_NAME_PREFIX, VALUE_TYPES # noqa: F401 +from .helpers import ( # noqa: F401 + get_annualized_variable, + get_neutralized_variable, + ) from .variable import Variable # noqa: F401 diff --git a/openfisca_core/variables/config.py b/openfisca_core/variables/config.py index b260bb3dd9..51f6ac6950 100644 --- a/openfisca_core/variables/config.py +++ b/openfisca_core/variables/config.py @@ -5,7 +5,6 @@ from openfisca_core import indexed_enums from openfisca_core.indexed_enums import Enum - VALUE_TYPES = { bool: { 'dtype': numpy.bool_, diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index 335a585498..05bd1e23a1 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -1,8 +1,9 @@ from __future__ import annotations -import sortedcontainers from typing import Optional +import sortedcontainers + from openfisca_core.periods import Period from .. import variables @@ -17,7 +18,7 @@ def get_annualized_variable(variable: variables.Variable, annualization_period: def make_annual_formula(original_formula, annualization_period = None): def annual_formula(population, period, parameters): - if period.start.month != 1 and (annualization_period is None or annualization_period.contains(period)): + if period.start.month != 1 and (annualization_period is None or period not in annualization_period): return population(variable.name, period.this_year.first_month) if original_formula.__code__.co_argcount == 2: return original_formula(population, period) diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 1e9fce3083..6438374045 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -1,5 +1,7 @@ from __future__ import annotations +from openfisca_core.periods.typing import Instant +from openfisca_core.types import Formula from typing import Optional, Union import datetime @@ -14,7 +16,6 @@ from openfisca_core.entities import Entity from openfisca_core.indexed_enums import Enum, EnumArray from openfisca_core.periods import Period -from openfisca_core.types import Formula, Instant from . import config, helpers @@ -39,7 +40,7 @@ class Variable: .. attribute:: definition_period - `Period `_ the variable is defined for. Possible value: ``MONTH``, ``YEAR``, ``ETERNITY``. + `Period `_ the variable is defined for. Possible value: ``DateUnit.DAY``, ``DateUnit.MONTH``, ``DateUnit.YEAR``, ``DateUnit.ETERNITY``. .. attribute:: formulas @@ -122,7 +123,7 @@ def __init__(self, baseline_variable = None): else: self.default_value = self.set(attr, 'default_value', allowed_type = self.value_type, default = config.VALUE_TYPES[self.value_type].get('default')) self.entity = self.set(attr, 'entity', required = True, setter = self.set_entity) - self.definition_period = self.set(attr, 'definition_period', required = True, allowed_values = (periods.DAY, periods.MONTH, periods.YEAR, periods.ETERNITY)) + self.definition_period = self.set(attr, 'definition_period', required = True, allowed_values = (periods.DateUnit.DAY, periods.DateUnit.MONTH, periods.DateUnit.YEAR, periods.DateUnit.ETERNITY)) self.label = self.set(attr, 'label', allowed_type = str, setter = self.set_label) self.end = self.set(attr, 'end', allowed_type = str, setter = self.set_end) self.reference = self.set(attr, 'reference', setter = self.set_reference) @@ -326,6 +327,8 @@ def get_formula( """ + instant: Optional[Instant] + if not self.formulas: return None @@ -334,13 +337,18 @@ def get_formula( if isinstance(period, Period): instant = period.start + else: try: - instant = periods.period(period).start + instant = periods.build_period(period).start + except ValueError: - instant = periods.instant(period) + instant = periods.build_instant(period) + + if instant is None: + return None - if self.end and instant.date > self.end: + if self.end and instant.date() > self.end: return None instant_str = str(instant) diff --git a/openfisca_tasks/lint.mk b/openfisca_tasks/lint.mk index 115c6267bb..7d546e0937 100644 --- a/openfisca_tasks/lint.mk +++ b/openfisca_tasks/lint.mk @@ -17,6 +17,7 @@ check-style: $(shell git ls-files "*.py") ## Run linters to check for syntax and style errors in the doc. lint-doc: \ lint-doc-commons \ + lint-doc-periods \ lint-doc-types \ ; @@ -42,6 +43,7 @@ check-types: ## Run static type checkers for type errors (strict). lint-typing-strict: \ lint-typing-strict-commons \ + lint-typing-strict-periods \ lint-typing-strict-types \ ; diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index 63fdd4386a..c60c294bf7 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -34,6 +34,7 @@ test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 @pytest --quiet --capture=no --xdoctest --xdoctest-verbose=0 \ openfisca_core/commons \ openfisca_core/holders \ + openfisca_core/periods \ openfisca_core/types @PYTEST_ADDOPTS="$${PYTEST_ADDOPTS} ${pytest_args}" \ coverage run -m \ diff --git a/openfisca_web_api/app.py b/openfisca_web_api/app.py index 2117fdb631..78e185be24 100644 --- a/openfisca_web_api/app.py +++ b/openfisca_web_api/app.py @@ -4,16 +4,16 @@ import os import traceback -from openfisca_core.errors import SituationParsingError, PeriodMismatchError -from openfisca_web_api.loader import build_data -from openfisca_web_api.errors import handle_import_error +from openfisca_core.errors import PeriodMismatchError, SituationParsingError from openfisca_web_api import handlers +from openfisca_web_api.errors import handle_import_error +from openfisca_web_api.loader import build_data try: - from flask import Flask, jsonify, abort, request, make_response + import werkzeug.exceptions + from flask import abort, Flask, jsonify, make_response, request from flask_cors import CORS from werkzeug.middleware.proxy_fix import ProxyFix - import werkzeug.exceptions except ImportError as error: handle_import_error(error) diff --git a/openfisca_web_api/handlers.py b/openfisca_web_api/handlers.py index 32a33a2a0b..4bc3527b53 100644 --- a/openfisca_web_api/handlers.py +++ b/openfisca_web_api/handlers.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- import dpath.util -from openfisca_core.simulation_builder import SimulationBuilder + from openfisca_core.indexed_enums import Enum +from openfisca_core.simulation_builder import SimulationBuilder def calculate(tax_benefit_system, input_data: dict) -> dict: diff --git a/openfisca_web_api/loader/__init__.py b/openfisca_web_api/loader/__init__.py index b86aefad57..d144df0058 100644 --- a/openfisca_web_api/loader/__init__.py +++ b/openfisca_web_api/loader/__init__.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- -from openfisca_web_api.loader.parameters import build_parameters -from openfisca_web_api.loader.variables import build_variables from openfisca_web_api.loader.entities import build_entities +from openfisca_web_api.loader.parameters import build_parameters from openfisca_web_api.loader.spec import build_openAPI_specification +from openfisca_web_api.loader.variables import build_variables def build_data(tax_benefit_system): diff --git a/openfisca_web_api/loader/spec.py b/openfisca_web_api/loader/spec.py index d36fd53aa6..7cc4314621 100644 --- a/openfisca_web_api/loader/spec.py +++ b/openfisca_web_api/loader/spec.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- import os -import yaml from copy import deepcopy import dpath.util +import yaml from openfisca_core.indexed_enums import Enum from openfisca_web_api import handlers - OPEN_API_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.path.pardir, 'openAPI.yml') diff --git a/openfisca_web_api/loader/tax_benefit_system.py b/openfisca_web_api/loader/tax_benefit_system.py index 856f760008..fe9a8b3e8a 100644 --- a/openfisca_web_api/loader/tax_benefit_system.py +++ b/openfisca_web_api/loader/tax_benefit_system.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- import importlib -import traceback import logging +import traceback from os import linesep log = logging.getLogger(__name__) diff --git a/openfisca_web_api/scripts/serve.py b/openfisca_web_api/scripts/serve.py index 428cf2b965..179b1f9c8c 100644 --- a/openfisca_web_api/scripts/serve.py +++ b/openfisca_web_api/scripts/serve.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- -import sys import logging +import sys from openfisca_core.scripts import build_tax_benefit_system from openfisca_web_api.app import create_app from openfisca_web_api.errors import handle_import_error try: - from gunicorn.app.base import BaseApplication from gunicorn import config + from gunicorn.app.base import BaseApplication except ImportError as error: handle_import_error(error) diff --git a/setup.cfg b/setup.cfg index 467e3ede59..9608c7cfe7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,13 +12,26 @@ [flake8] extend-ignore = D hang-closing = true -ignore = E128,E251,F403,F405,E501,RST301,W503,W504 +ignore = E128,E251,F403,F405,E501,E704,RST301,W503,W504 in-place = true -include-in-doctest = openfisca_core/commons openfisca_core/holders openfisca_core/types +include-in-doctest = openfisca_core/commons openfisca_core/holders openfisca_core/periods openfisca_core/types +per-file-ignores = */typing.py:D101,D102,E704, */__init__.py:F401 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, mod, obj strictness = short +[isort] +case_sensitive = true +force_alphabetical_sort_within_sections = true +group_by_package = true +include_trailing_comma = true +known_first_party = openfisca_core +known_openfisca = openfisca_country_template, openfisca_extension_template +known_typing = mypy*, *types*, *typing* +multi_line_output = 8 +py_version = 37 +sections = FUTURE,TYPING,STDLIB,THIRDPARTY,OPENFISCA,FIRSTPARTY,LOCALFOLDER + [pylint.message_control] disable = all enable = C0115,C0116,R0401 @@ -41,17 +54,14 @@ skip_empty = true addopts = --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py -testpaths = tests +testpaths = openfisca_core/commons/tests openfisca_core/holders/tests openfisca_core/periods/tests tests [mypy] ignore_missing_imports = True install_types = True non_interactive = True -[mypy-openfisca_core.commons.tests.*] -ignore_errors = True - -[mypy-openfisca_core.holders.tests.*] +[mypy-openfisca_core.*.tests.*] ignore_errors = True [mypy-openfisca_core.scripts.*] diff --git a/setup.py b/setup.py index 2543121ac8..3b17d1d809 100644 --- a/setup.py +++ b/setup.py @@ -15,9 +15,10 @@ """ -from setuptools import setup, find_packages from pathlib import Path +from setuptools import find_packages, setup + # Read the contents of our README file for PyPi this_directory = Path(__file__).parent long_description = (this_directory / "README.md").read_text() @@ -26,13 +27,16 @@ # functional and integration breaks caused by external code updates. general_requirements = [ + 'PyYAML >= 3.10', + 'StrEnum >= 0.4.8, < 0.5.0', # 3.11.x backport 'dpath >= 1.5.0, < 3.0.0', + 'importlib-metadata < 4.3.0', 'nptyping == 1.4.4', 'numexpr >= 2.7.0, <= 3.0', - 'numpy >= 1.11, < 1.21', + 'numpy >= 1.20, < 1.21', + 'pendulum >= 2.1.0, < 3.0.0', 'psutil >= 5.4.7, < 6.0.0', 'pytest >= 4.4.1, < 6.0.0', # For openfisca test - 'PyYAML >= 3.10', 'sortedcontainers == 2.2.2', 'typing-extensions >= 4.0.0, < 5.0.0', ] @@ -63,7 +67,7 @@ setup( name = 'OpenFisca-Core', - version = '38.0.0', + version = '39.1.0', author = 'OpenFisca Team', author_email = 'contact@openfisca.org', classifiers = [ diff --git a/tests/core/parameter_validation/test_parameter_validation.py b/tests/core/parameter_validation/test_parameter_validation.py index 62b2b0c132..49e56c6e42 100644 --- a/tests/core/parameter_validation/test_parameter_validation.py +++ b/tests/core/parameter_validation/test_parameter_validation.py @@ -1,8 +1,14 @@ # -*- coding: utf-8 -*- import os + import pytest -from openfisca_core.parameters import load_parameter_file, ParameterNode, ParameterParsingError + +from openfisca_core.parameters import ( + load_parameter_file, + ParameterNode, + ParameterParsingError, + ) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) year = 2016 diff --git a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py index b14da9ce62..a10ab68021 100644 --- a/tests/core/parameters_fancy_indexing/test_fancy_indexing.py +++ b/tests/core/parameters_fancy_indexing/test_fancy_indexing.py @@ -6,10 +6,13 @@ import numpy import pytest - +from openfisca_core.indexed_enums import Enum +from openfisca_core.parameters import ( + Parameter, + ParameterNode, + ParameterNotFound, + ) from openfisca_core.tools import assert_near -from openfisca_core.parameters import ParameterNode, Parameter, ParameterNotFound -from openfisca_core.model_api import * # noqa LOCAL_DIR = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/core/tax_scales/test_abstract_rate_tax_scale.py b/tests/core/tax_scales/test_abstract_rate_tax_scale.py index 3d284a49e9..1935442f63 100644 --- a/tests/core/tax_scales/test_abstract_rate_tax_scale.py +++ b/tests/core/tax_scales/test_abstract_rate_tax_scale.py @@ -1,7 +1,7 @@ -from openfisca_core import taxscales - import pytest +from openfisca_core import taxscales + def test_abstract_tax_scale(): with pytest.warns(DeprecationWarning): diff --git a/tests/core/tax_scales/test_abstract_tax_scale.py b/tests/core/tax_scales/test_abstract_tax_scale.py index f6834e7dc7..805841b113 100644 --- a/tests/core/tax_scales/test_abstract_tax_scale.py +++ b/tests/core/tax_scales/test_abstract_tax_scale.py @@ -1,7 +1,7 @@ -from openfisca_core import taxscales - import pytest +from openfisca_core import taxscales + def test_abstract_tax_scale(): with pytest.warns(DeprecationWarning): diff --git a/tests/core/tax_scales/test_linear_average_rate_tax_scale.py b/tests/core/tax_scales/test_linear_average_rate_tax_scale.py index 83153024c7..448c89ca40 100644 --- a/tests/core/tax_scales/test_linear_average_rate_tax_scale.py +++ b/tests/core/tax_scales/test_linear_average_rate_tax_scale.py @@ -1,10 +1,8 @@ import numpy - -from openfisca_core import taxscales -from openfisca_core import tools - import pytest +from openfisca_core import taxscales, tools + def test_bracket_indices(): tax_base = numpy.array([0, 1, 2, 3, 4, 5]) diff --git a/tests/core/tax_scales/test_marginal_amount_tax_scale.py b/tests/core/tax_scales/test_marginal_amount_tax_scale.py index 7582d725b4..48de2524d0 100644 --- a/tests/core/tax_scales/test_marginal_amount_tax_scale.py +++ b/tests/core/tax_scales/test_marginal_amount_tax_scale.py @@ -1,12 +1,8 @@ from numpy import array - -from openfisca_core import parameters -from openfisca_core import periods -from openfisca_core import taxscales -from openfisca_core import tools - from pytest import fixture +from openfisca_core import parameters, periods, taxscales, tools + @fixture def data(): diff --git a/tests/core/tax_scales/test_marginal_rate_tax_scale.py b/tests/core/tax_scales/test_marginal_rate_tax_scale.py index 41cb6c804d..c984a03a4c 100644 --- a/tests/core/tax_scales/test_marginal_rate_tax_scale.py +++ b/tests/core/tax_scales/test_marginal_rate_tax_scale.py @@ -1,10 +1,8 @@ import numpy - -from openfisca_core import taxscales -from openfisca_core import tools - import pytest +from openfisca_core import taxscales, tools + def test_bracket_indices(): tax_base = numpy.array([0, 1, 2, 3, 4, 5]) diff --git a/tests/core/tax_scales/test_single_amount_tax_scale.py b/tests/core/tax_scales/test_single_amount_tax_scale.py index c5e6483a7d..bb58e2dfa9 100644 --- a/tests/core/tax_scales/test_single_amount_tax_scale.py +++ b/tests/core/tax_scales/test_single_amount_tax_scale.py @@ -1,12 +1,8 @@ import numpy - -from openfisca_core import parameters -from openfisca_core import periods -from openfisca_core import taxscales -from openfisca_core import tools - from pytest import fixture +from openfisca_core import parameters, periods, taxscales, tools + @fixture def data(): diff --git a/tests/core/tax_scales/test_tax_scales_commons.py b/tests/core/tax_scales/test_tax_scales_commons.py index d45bdd894a..572149cb65 100644 --- a/tests/core/tax_scales/test_tax_scales_commons.py +++ b/tests/core/tax_scales/test_tax_scales_commons.py @@ -1,9 +1,7 @@ -from openfisca_core import parameters -from openfisca_core import taxscales -from openfisca_core import tools - import pytest +from openfisca_core import parameters, taxscales, tools + @pytest.fixture def node(): diff --git a/tests/core/test_axes.py b/tests/core/test_axes.py index f106a82a5b..d3283eb578 100644 --- a/tests/core/test_axes.py +++ b/tests/core/test_axes.py @@ -3,7 +3,6 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.tools import test_runner - # With periods diff --git a/tests/core/test_calculate_output.py b/tests/core/test_calculate_output.py index 6a11a27d84..e5dd0d9a43 100644 --- a/tests/core/test_calculate_output.py +++ b/tests/core/test_calculate_output.py @@ -9,20 +9,20 @@ class simple_variable(Variable): entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH value_type = int class variable_with_calculate_output_add(Variable): entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH value_type = int calculate_output = simulations.calculate_output_add class variable_with_calculate_output_divide(Variable): entity = entities.Person - definition_period = periods.YEAR + definition_period = periods.DateUnit.YEAR value_type = int calculate_output = simulations.calculate_output_divide diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index aeb4d762c7..ebe8e90805 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -1,11 +1,14 @@ import pytest from openfisca_core import periods, populations, tools -from openfisca_core.errors import VariableNameConflictError, VariableNotFoundError +from openfisca_core.errors import ( + VariableNameConflictError, + VariableNotFoundError, + ) from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable -PERIOD = periods.period("2016-01") +PERIOD = periods.build_period("2016-01") @pytest.mark.parametrize("simulation", [({"salary": 2000}, PERIOD)], indirect = True) @@ -102,7 +105,7 @@ def test_variable_with_reference(make_simulation, isolated_tax_benefit_system): assert result > 0 class disposable_income(Variable): - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(household, period): return household.empty_array() @@ -119,7 +122,7 @@ def test_variable_name_conflict(tax_benefit_system): class disposable_income(Variable): reference = "disposable_income" - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(household, period): return household.empty_array() diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index 1c4361ded2..83cf042d22 100644 --- a/tests/core/test_cycles.py +++ b/tests/core/test_cycles.py @@ -10,7 +10,7 @@ @pytest.fixture def reference_period(): - return periods.period('2013-01') + return periods.build_period('2013-01') @pytest.fixture @@ -22,7 +22,7 @@ def simulation(tax_benefit_system): class variable1(Variable): value_type = int entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): return person('variable2', period) @@ -31,7 +31,7 @@ def formula(person, period): class variable2(Variable): value_type = int entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): return person('variable1', period) @@ -41,7 +41,7 @@ def formula(person, period): class variable3(Variable): value_type = int entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): return person('variable4', period.last_month) @@ -50,7 +50,7 @@ def formula(person, period): class variable4(Variable): value_type = int entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): return person('variable3', period) @@ -61,7 +61,7 @@ def formula(person, period): class variable5(Variable): value_type = int entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): variable6 = person('variable6', period.last_month) @@ -71,7 +71,7 @@ def formula(person, period): class variable6(Variable): value_type = int entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): variable5 = person('variable5', period) @@ -81,7 +81,7 @@ def formula(person, period): class variable7(Variable): value_type = int entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): variable5 = person('variable5', period) @@ -92,7 +92,7 @@ def formula(person, period): class cotisation(Variable): value_type = int entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): if period.start.month == 12: diff --git a/tests/core/test_formulas.py b/tests/core/test_formulas.py index 8851671755..ebc126fd1c 100644 --- a/tests/core/test_formulas.py +++ b/tests/core/test_formulas.py @@ -1,4 +1,5 @@ import numpy +from pytest import approx, fixture from openfisca_country_template import entities @@ -6,20 +7,18 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable -from pytest import fixture, approx - class choice(Variable): value_type = int entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH class uses_multiplication(Variable): value_type = int entity = entities.Person label = 'Variable with formula that uses multiplication' - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): choice = person('choice', period) @@ -31,7 +30,7 @@ class returns_scalar(Variable): value_type = int entity = entities.Person label = 'Variable with formula that returns a scalar value' - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): return 666 @@ -41,7 +40,7 @@ class uses_switch(Variable): value_type = int entity = entities.Person label = 'Variable with formula that uses switch' - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): choice = person('choice', period) @@ -104,9 +103,8 @@ def test_group_encapsulation(): And calculations are projected to all the member families. """ - from openfisca_core.taxbenefitsystems import TaxBenefitSystem from openfisca_core.entities import build_entity - from openfisca_core.periods import ETERNITY + from openfisca_core.taxbenefitsystems import TaxBenefitSystem person_entity = build_entity( key="person", @@ -143,12 +141,12 @@ def test_group_encapsulation(): class household_level_variable(Variable): value_type = int entity = household_entity - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY class projected_family_level_variable(Variable): value_type = int entity = family_entity - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY def formula(family, period): return family.household("household_level_variable", period) diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index 907aefceb5..de479808b7 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -1,15 +1,14 @@ -import pytest - import numpy +import pytest from openfisca_country_template import situation_examples from openfisca_country_template.variables import housing from openfisca_core import holders, periods, tools from openfisca_core.errors import PeriodMismatchError +from openfisca_core.holders import Holder from openfisca_core.memory_config import MemoryConfig from openfisca_core.simulations import SimulationBuilder -from openfisca_core.holders import Holder @pytest.fixture @@ -26,7 +25,7 @@ def couple(tax_benefit_system): build_from_entities(tax_benefit_system, situation_examples.couple) -period = periods.period('2017-12') +period = periods.build_period('2017-12') def test_set_input_enum_string(couple): @@ -89,17 +88,17 @@ def test_permanent_variable_filled(single): simulation = single holder = simulation.person.get_holder('birth') value = numpy.asarray(['1980-01-01'], dtype = holder.variable.dtype) - holder.set_input(periods.period(periods.ETERNITY), value) + holder.set_input(periods.build_period(periods.DateUnit.ETERNITY), value) assert holder.get_array(None) == value - assert holder.get_array(periods.ETERNITY) == value + assert holder.get_array(periods.DateUnit.ETERNITY) == value assert holder.get_array('2016-01') == value def test_delete_arrays(single): simulation = single salary_holder = simulation.person.get_holder('salary') - salary_holder.set_input(periods.period(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.period(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.build_period(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.build_period(2018), numpy.asarray([60000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 5000 salary_holder.delete_arrays(period = 2018) @@ -109,7 +108,7 @@ def test_delete_arrays(single): salary_array = simulation.get_array('salary', '2018-01') assert salary_array is None - salary_holder.set_input(periods.period(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.build_period(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -119,7 +118,7 @@ def test_get_memory_usage(single): salary_holder = simulation.person.get_holder('salary') memory_usage = salary_holder.get_memory_usage() assert memory_usage['total_nb_bytes'] == 0 - salary_holder.set_input(periods.period(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.build_period(2017), numpy.asarray([30000])) memory_usage = salary_holder.get_memory_usage() assert memory_usage['nb_cells_by_array'] == 1 assert memory_usage['cell_size'] == 4 # float 32 @@ -132,7 +131,7 @@ def test_get_memory_usage_with_trace(single): simulation = single simulation.trace = True salary_holder = simulation.person.get_holder('salary') - salary_holder.set_input(periods.period(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.build_period(2017), numpy.asarray([30000])) simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-02') @@ -147,7 +146,7 @@ def test_set_input_dispatch_by_period(single): variable = simulation.tax_benefit_system.get_variable('housing_occupancy_status') entity = simulation.household holder = Holder(variable, entity) - holders.set_input_dispatch_by_period(holder, periods.period(2019), 'owner') + holders.set_input_dispatch_by_period(holder, periods.build_period(2019), 'owner') assert holder.get_array('2019-01') == holder.get_array('2019-12') # Check the feature assert holder.get_array('2019-01') is holder.get_array('2019-12') # Check that the vectors are the same in memory, to avoid duplication @@ -159,12 +158,12 @@ def test_delete_arrays_on_disk(single): simulation = single simulation.memory_config = force_storage_on_disk salary_holder = simulation.person.get_holder('salary') - salary_holder.set_input(periods.period(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.period(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.build_period(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.build_period(2018), numpy.asarray([60000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 5000 salary_holder.delete_arrays(period = 2018) - salary_holder.set_input(periods.period(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.build_period(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -172,7 +171,7 @@ def test_delete_arrays_on_disk(single): def test_cache_disk(couple): simulation = couple simulation.memory_config = force_storage_on_disk - month = periods.period('2017-01') + month = periods.build_period('2017-01') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -183,8 +182,8 @@ def test_cache_disk(couple): def test_known_periods(couple): simulation = couple simulation.memory_config = force_storage_on_disk - month = periods.period('2017-01') - month_2 = periods.period('2017-02') + month = periods.build_period('2017-01') + month_2 = periods.build_period('2017-02') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -196,7 +195,7 @@ def test_known_periods(couple): def test_cache_enum_on_disk(single): simulation = single simulation.memory_config = force_storage_on_disk - month = periods.period('2017-01') + month = periods.build_period('2017-01') simulation.calculate('housing_occupancy_status', month) # First calculation housing_occupancy_status = simulation.calculate('housing_occupancy_status', month) # Read from cache assert housing_occupancy_status == housing.HousingOccupancyStatus.tenant diff --git a/tests/core/test_opt_out_cache.py b/tests/core/test_opt_out_cache.py index b4eab3e5a5..ee22be42d8 100644 --- a/tests/core/test_opt_out_cache.py +++ b/tests/core/test_opt_out_cache.py @@ -3,25 +3,23 @@ from openfisca_country_template.entities import Person from openfisca_core import periods -from openfisca_core.periods import MONTH from openfisca_core.variables import Variable - -PERIOD = periods.period("2016-01") +PERIOD = periods.build_period("2016-01") class input(Variable): value_type = int entity = Person label = "Input variable" - definition_period = MONTH + definition_period = periods.DateUnit.MONTH class intermediate(Variable): value_type = int entity = Person label = "Intermediate result that don't need to be cached" - definition_period = MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): return person('input', period) @@ -31,7 +29,7 @@ class output(Variable): value_type = int entity = Person label = 'Output variable' - definition_period = MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period): return person('intermediate', period) diff --git a/tests/core/test_parameters.py b/tests/core/test_parameters.py index 40d8bb3fc9..b44490f0ee 100644 --- a/tests/core/test_parameters.py +++ b/tests/core/test_parameters.py @@ -2,7 +2,12 @@ import pytest -from openfisca_core.parameters import ParameterNotFound, ParameterNode, ParameterNodeAtInstant, load_parameter_file +from openfisca_core.parameters import ( + load_parameter_file, + ParameterNode, + ParameterNodeAtInstant, + ParameterNotFound, + ) def test_get_at_instant(tax_benefit_system): diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py deleted file mode 100644 index 2c125d527c..0000000000 --- a/tests/core/test_periods.py +++ /dev/null @@ -1,203 +0,0 @@ -# -*- coding: utf-8 -*- - - -import pytest - -from openfisca_core.periods import Period, Instant, YEAR, MONTH, DAY, period - -first_jan = Instant((2014, 1, 1)) -first_march = Instant((2014, 3, 1)) - - -''' -Test Period -> String -''' - - -# Years - -def test_year(): - assert str(Period((YEAR, first_jan, 1))) == '2014' - - -def test_12_months_is_a_year(): - assert str(Period((MONTH, first_jan, 12))) == '2014' - - -def test_rolling_year(): - assert str(Period((MONTH, first_march, 12))) == 'year:2014-03' - assert str(Period((YEAR, first_march, 1))) == 'year:2014-03' - - -def test_several_years(): - assert str(Period((YEAR, first_jan, 3))) == 'year:2014:3' - assert str(Period((YEAR, first_march, 3))) == 'year:2014-03:3' - - -# Months - -def test_month(): - assert str(Period((MONTH, first_jan, 1))) == '2014-01' - - -def test_several_months(): - assert str(Period((MONTH, first_jan, 3))) == 'month:2014-01:3' - assert str(Period((MONTH, first_march, 3))) == 'month:2014-03:3' - - -# Days - -def test_day(): - assert str(Period((DAY, first_jan, 1))) == '2014-01-01' - - -def test_several_days(): - assert str(Period((DAY, first_jan, 3))) == 'day:2014-01-01:3' - assert str(Period((DAY, first_march, 3))) == 'day:2014-03-01:3' - - -''' -Test String -> Period -''' - - -# Years - -def test_parsing_year(): - assert period('2014') == Period((YEAR, first_jan, 1)) - - -def test_parsing_rolling_year(): - assert period('year:2014-03') == Period((YEAR, first_march, 1)) - - -def test_parsing_several_years(): - assert period('year:2014:2') == Period((YEAR, first_jan, 2)) - - -def test_wrong_syntax_several_years(): - with pytest.raises(ValueError): - period('2014:2') - - -# Months - -def test_parsing_month(): - assert period('2014-01') == Period((MONTH, first_jan, 1)) - - -def test_parsing_several_months(): - assert period('month:2014-03:3') == Period((MONTH, first_march, 3)) - - -def test_wrong_syntax_several_months(): - with pytest.raises(ValueError): - period('2014-3:3') - - -# Days - -def test_parsing_day(): - assert period('2014-01-01') == Period((DAY, first_jan, 1)) - - -def test_parsing_several_days(): - assert period('day:2014-03-01:3') == Period((DAY, first_march, 3)) - - -def test_wrong_syntax_several_days(): - with pytest.raises(ValueError): - period('2014-2-3:2') - - -def test_day_size_in_days(): - assert Period(('day', Instant((2014, 12, 31)), 1)).size_in_days == 1 - - -def test_3_day_size_in_days(): - assert Period(('day', Instant((2014, 12, 31)), 3)).size_in_days == 3 - - -def test_month_size_in_days(): - assert Period(('month', Instant((2014, 12, 1)), 1)).size_in_days == 31 - - -def test_leap_month_size_in_days(): - assert Period(('month', Instant((2012, 2, 3)), 1)).size_in_days == 29 - - -def test_3_month_size_in_days(): - assert Period(('month', Instant((2013, 1, 3)), 3)).size_in_days == 31 + 28 + 31 - - -def test_leap_3_month_size_in_days(): - assert Period(('month', Instant((2012, 1, 3)), 3)).size_in_days == 31 + 29 + 31 - - -def test_year_size_in_days(): - assert Period(('year', Instant((2014, 12, 1)), 1)).size_in_days == 365 - - -def test_leap_year_size_in_days(): - assert Period(('year', Instant((2012, 1, 1)), 1)).size_in_days == 366 - - -def test_2_years_size_in_days(): - assert Period(('year', Instant((2014, 1, 1)), 2)).size_in_days == 730 - -# Misc - - -def test_wrong_date(): - with pytest.raises(ValueError): - period("2006-31-03") - - -def test_ambiguous_period(): - with pytest.raises(ValueError): - period('month:2014') - - -def test_deprecated_signature(): - with pytest.raises(TypeError): - period(MONTH, 2014) - - -def test_wrong_argument(): - with pytest.raises(ValueError): - period({}) - - -def test_wrong_argument_1(): - with pytest.raises(ValueError): - period([]) - - -def test_none(): - with pytest.raises(ValueError): - period(None) - - -def test_empty_string(): - with pytest.raises(ValueError): - period('') - - -@pytest.mark.parametrize("test", [ - (period('year:2014:2'), YEAR, 2, period('2014'), period('2015')), - (period(2017), MONTH, 12, period('2017-01'), period('2017-12')), - (period('year:2014:2'), MONTH, 24, period('2014-01'), period('2015-12')), - (period('month:2014-03:3'), MONTH, 3, period('2014-03'), period('2014-05')), - (period(2017), DAY, 365, period('2017-01-01'), period('2017-12-31')), - (period('year:2014:2'), DAY, 730, period('2014-01-01'), period('2015-12-31')), - (period('month:2014-03:3'), DAY, 92, period('2014-03-01'), period('2014-05-31')), - ]) -def test_subperiods(test): - - def check_subperiods(period, unit, length, first, last): - subperiods = period.get_subperiods(unit) - assert len(subperiods) == length - assert subperiods[0] == first - assert subperiods[-1] == last - - check_subperiods(*test) diff --git a/tests/core/test_projectors.py b/tests/core/test_projectors.py index 1fa3a759b3..ae0538308e 100644 --- a/tests/core/test_projectors.py +++ b/tests/core/test_projectors.py @@ -1,8 +1,11 @@ +import numpy + +from openfisca_core import periods +from openfisca_core.entities import build_entity +from openfisca_core.indexed_enums import Enum from openfisca_core.simulations.simulation_builder import SimulationBuilder from openfisca_core.taxbenefitsystems import TaxBenefitSystem -from openfisca_core.entities import build_entity -from openfisca_core.model_api import Enum, Variable, ETERNITY -import numpy +from openfisca_core.variables import Variable def test_shortcut_to_containing_entity_provided(): @@ -125,14 +128,14 @@ class household_enum_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = household - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY class projected_enum_variable(Variable): value_type = Enum possible_values = enum default_value = enum.FIRST_OPTION entity = person - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY def formula(person, period): return person.household("household_enum_variable", period) @@ -194,7 +197,7 @@ class household_projected_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = household - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY def formula(household, period): return household.value_from_first_person(household.members("person_enum_variable", period)) @@ -204,7 +207,7 @@ class person_enum_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = person - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY system.add_variables(household_projected_variable, person_enum_variable) @@ -275,14 +278,14 @@ class household_level_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = household_entity - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY class projected_family_level_variable(Variable): value_type = Enum possible_values = enum default_value = enum.FIRST_OPTION entity = family_entity - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY def formula(family, period): return family.household("household_level_variable", period) @@ -290,7 +293,7 @@ def formula(family, period): class decoded_projected_family_level_variable(Variable): value_type = str entity = family_entity - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY def formula(family, period): return family.household("household_level_variable", period).decode_to_str() diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index 8735cee18f..5ea293346a 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -2,12 +2,14 @@ import pytest -from openfisca_core import periods +from openfisca_country_template.entities import Household, Person + +from openfisca_core import holders, periods, simulations +from openfisca_core.parameters import ParameterNode, ValuesHistory from openfisca_core.periods import Instant +from openfisca_core.reforms import Reform from openfisca_core.tools import assert_near -from openfisca_core.parameters import ValuesHistory, ParameterNode -from openfisca_country_template.entities import Household, Person -from openfisca_core.model_api import * # noqa analysis:ignore +from openfisca_core.variables import Variable class goes_to_school(Variable): @@ -15,7 +17,7 @@ class goes_to_school(Variable): default_value = True entity = Person label = "The person goes to school (only relevant for children)" - definition_period = MONTH + definition_period = periods.DateUnit.MONTH class WithBasicIncomeNeutralized(Reform): @@ -124,23 +126,23 @@ def check_update_items(description, value_history, start_instant, stop_instant, check_update_items( 'Replace an item by a new item', ValuesHistory('dummy_name', {"2013-01-01": {'value': 0.0}, "2014-01-01": {'value': None}}), - periods.period(2013).start, - periods.period(2013).stop, + periods.build_period(2013).start, + periods.build_period(2013).stop, 1.0, ValuesHistory('dummy_name', {"2013-01-01": {'value': 1.0}, "2014-01-01": {'value': None}}), ) check_update_items( 'Replace an item by a new item in a list of items, the last being open', ValuesHistory('dummy_name', {"2014-01-01": {'value': 9.53}, "2015-01-01": {'value': 9.61}, "2016-01-01": {'value': 9.67}}), - periods.period(2015).start, - periods.period(2015).stop, + periods.build_period(2015).start, + periods.build_period(2015).stop, 1.0, ValuesHistory('dummy_name', {"2014-01-01": {'value': 9.53}, "2015-01-01": {'value': 1.0}, "2016-01-01": {'value': 9.67}}), ) check_update_items( 'Open the stop instant to the future', ValuesHistory('dummy_name', {"2013-01-01": {'value': 0.0}, "2014-01-01": {'value': None}}), - periods.period(2013).start, + periods.build_period(2013).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2013-01-01": {'value': 1.0}}), @@ -148,15 +150,15 @@ def check_update_items(description, value_history, start_instant, stop_instant, check_update_items( 'Insert a new item in the middle of an existing item', ValuesHistory('dummy_name', {"2010-01-01": {'value': 0.0}, "2014-01-01": {'value': None}}), - periods.period(2011).start, - periods.period(2011).stop, + periods.build_period(2011).start, + periods.build_period(2011).stop, 1.0, ValuesHistory('dummy_name', {"2010-01-01": {'value': 0.0}, "2011-01-01": {'value': 1.0}, "2012-01-01": {'value': 0.0}, "2014-01-01": {'value': None}}), ) check_update_items( 'Insert a new open item coming after the last open item', ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}}), - periods.period(2015).start, + periods.build_period(2015).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}, "2015-01-01": {'value': 1.0}}), @@ -164,15 +166,15 @@ def check_update_items(description, value_history, start_instant, stop_instant, check_update_items( 'Insert a new item starting at the same date than the last open item', ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}}), - periods.period(2014).start, - periods.period(2014).stop, + periods.build_period(2014).start, + periods.build_period(2014).stop, 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 1.0}, "2015-01-01": {'value': 0.14}}), ) check_update_items( 'Insert a new open item starting at the same date than the last open item', ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}}), - periods.period(2014).start, + periods.build_period(2014).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 1.0}}), @@ -180,23 +182,23 @@ def check_update_items(description, value_history, start_instant, stop_instant, check_update_items( 'Insert a new item coming before the first item', ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}}), - periods.period(2005).start, - periods.period(2005).stop, + periods.build_period(2005).start, + periods.build_period(2005).stop, 1.0, ValuesHistory('dummy_name', {"2005-01-01": {'value': 1.0}, "2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}}), ) check_update_items( 'Insert a new item coming before the first item with a hole', ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}}), - periods.period(2003).start, - periods.period(2003).stop, + periods.build_period(2003).start, + periods.build_period(2003).stop, 1.0, ValuesHistory('dummy_name', {"2003-01-01": {'value': 1.0}, "2004-01-01": {'value': None}, "2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}}), ) check_update_items( 'Insert a new open item starting before the start date of the first item', ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}}), - periods.period(2005).start, + periods.build_period(2005).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2005-01-01": {'value': 1.0}}), @@ -204,7 +206,7 @@ def check_update_items(description, value_history, start_instant, stop_instant, check_update_items( 'Insert a new open item starting at the same date than the first item', ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 0.14}}), - periods.period(2006).start, + periods.build_period(2006).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 1.0}}), @@ -216,7 +218,7 @@ class new_variable(Variable): value_type = int label = "Nouvelle variable introduite par la réforme" entity = Household - definition_period = MONTH + definition_period = periods.DateUnit.MONTH def formula(household, period): return household.empty_array() + 10 @@ -240,7 +242,7 @@ class new_dated_variable(Variable): value_type = int label = "Nouvelle variable introduite par la réforme" entity = Household - definition_period = MONTH + definition_period = periods.DateUnit.MONTH def formula_2010_01_01(household, period): return household.empty_array() + 10 @@ -263,7 +265,7 @@ def apply(self): def test_update_variable(make_simulation, tax_benefit_system): class disposable_income(Variable): - definition_period = MONTH + definition_period = periods.DateUnit.MONTH def formula_2018(household, period): return household.empty_array() + 10 @@ -294,7 +296,7 @@ def apply(self): def test_replace_variable(tax_benefit_system): class disposable_income(Variable): - definition_period = MONTH + definition_period = periods.DateUnit.MONTH entity = Person label = "Disposable income" value_type = float @@ -355,9 +357,9 @@ class some_variable(Variable): value_type = int entity = Person label = "Variable with many attributes" - definition_period = MONTH - set_input = set_input_divide_by_period - calculate_output = calculate_output_add + definition_period = periods.DateUnit.MONTH + set_input = holders.set_input_divide_by_period + calculate_output = simulations.calculate_output_add tax_benefit_system.add_variable(some_variable) diff --git a/tests/core/test_simulation_builder.py b/tests/core/test_simulation_builder.py index b6a558751d..24f08f697d 100644 --- a/tests/core/test_simulation_builder.py +++ b/tests/core/test_simulation_builder.py @@ -1,6 +1,7 @@ -import datetime from typing import Iterable +import datetime + import pytest from openfisca_country_template import entities, situation_examples @@ -18,7 +19,7 @@ def int_variable(persons): class intvar(Variable): - definition_period = periods.ETERNITY + definition_period = periods.DateUnit.ETERNITY value_type = int entity = persons @@ -32,7 +33,7 @@ def __init__(self): def date_variable(persons): class datevar(Variable): - definition_period = periods.ETERNITY + definition_period = periods.DateUnit.ETERNITY value_type = datetime.date entity = persons @@ -46,7 +47,7 @@ def __init__(self): def enum_variable(): class TestEnum(Variable): - definition_period = periods.ETERNITY + definition_period = periods.DateUnit.ETERNITY value_type = Enum dtype = 'O' default_value = '0' diff --git a/tests/core/test_tracers.py b/tests/core/test_tracers.py index 1746192d00..4439af3d97 100644 --- a/tests/core/test_tracers.py +++ b/tests/core/test_tracers.py @@ -1,14 +1,22 @@ # -*- coding: utf-8 -*- +import csv import json import os -import csv + import numpy -from pytest import fixture, mark, raises, approx +from pytest import approx, fixture, mark, raises -from openfisca_core.simulations import Simulation, CycleError, SpiralError -from openfisca_core.tracers import SimpleTracer, FullTracer, TracingParameterNodeAtInstant, TraceNode from openfisca_country_template.variables.housing import HousingOccupancyStatus + +from openfisca_core.simulations import CycleError, Simulation, SpiralError +from openfisca_core.tracers import ( + FullTracer, + SimpleTracer, + TraceNode, + TracingParameterNodeAtInstant, + ) + from .parameters_fancy_indexing.test_fancy_indexing import parameters diff --git a/tests/core/test_yaml.py b/tests/core/test_yaml.py index f63e37ff39..10620dfa4f 100644 --- a/tests/core/test_yaml.py +++ b/tests/core/test_yaml.py @@ -2,10 +2,10 @@ import subprocess import pytest + import openfisca_extension_template from openfisca_core.tools.test_runner import run_tests - from tests.fixtures import yaml_tests yaml_tests_dir = os.path.dirname(yaml_tests.__file__) diff --git a/tests/core/tools/test_runner/test_yaml_runner.py b/tests/core/tools/test_runner/test_yaml_runner.py index b836bc5e51..2737d4e0a9 100644 --- a/tests/core/tools/test_runner/test_yaml_runner.py +++ b/tests/core/tools/test_runner/test_yaml_runner.py @@ -1,15 +1,20 @@ -import os from typing import List -import pytest +import os + import numpy +import pytest -from openfisca_core.tools.test_runner import _get_tax_benefit_system, YamlItem, YamlFile +from openfisca_core import periods +from openfisca_core.entities import Entity from openfisca_core.errors import VariableNotFound -from openfisca_core.variables import Variable from openfisca_core.populations import Population -from openfisca_core.entities import Entity -from openfisca_core.periods import ETERNITY +from openfisca_core.tools.test_runner import ( + _get_tax_benefit_system, + YamlFile, + YamlItem, + ) +from openfisca_core.variables import Variable class TaxBenefitSystem: @@ -73,7 +78,7 @@ def __init__(self, test): class TestVariable(Variable): - definition_period = ETERNITY + definition_period = periods.DateUnit.ETERNITY value_type = float def __init__(self): diff --git a/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index 056fcfead3..5ad25d6f94 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -1,10 +1,10 @@ import numpy from pytest import fixture -from openfisca_core import periods -from openfisca_core.model_api import * # noqa analysis:ignore from openfisca_country_template.entities import Person -from openfisca_core.variables import get_annualized_variable + +from openfisca_core import periods +from openfisca_core.variables import get_annualized_variable, Variable @fixture @@ -15,7 +15,7 @@ def monthly_variable(): class monthly_variable(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH def formula(person, period, parameters): variable.calculation_count += 1 @@ -41,13 +41,13 @@ def __call__(self, variable_name: str, period): def test_without_annualize(monthly_variable): - period = periods.period(2019) + period = periods.build_period(2019) person = PopulationMock(monthly_variable) yearly_sum = sum( person('monthly_variable', month) - for month in period.get_subperiods(MONTH) + for month in period.subperiods(periods.DateUnit.MONTH) ) assert monthly_variable.calculation_count == 11 @@ -55,14 +55,14 @@ def test_without_annualize(monthly_variable): def test_with_annualize(monthly_variable): - period = periods.period(2019) + period = periods.build_period(2019) annualized_variable = get_annualized_variable(monthly_variable) person = PopulationMock(annualized_variable) yearly_sum = sum( person('monthly_variable', month) - for month in period.get_subperiods(MONTH) + for month in period.subperiods(periods.DateUnit.MONTH) ) assert monthly_variable.calculation_count == 0 @@ -70,14 +70,14 @@ def test_with_annualize(monthly_variable): def test_with_partial_annualize(monthly_variable): - period = periods.period('year:2018:2') - annualized_variable = get_annualized_variable(monthly_variable, periods.period(2018)) + period = periods.build_period('year:2018:2') + annualized_variable = get_annualized_variable(monthly_variable, periods.build_period(2018)) person = PopulationMock(annualized_variable) yearly_sum = sum( person('monthly_variable', month) - for month in period.get_subperiods(MONTH) + for month in period.subperiods(periods.DateUnit.MONTH) ) assert monthly_variable.calculation_count == 11 diff --git a/tests/core/variables/test_variables.py b/tests/core/variables/test_variables.py index 876145bde1..1883f1b881 100644 --- a/tests/core/variables/test_variables.py +++ b/tests/core/variables/test_variables.py @@ -1,17 +1,15 @@ -# -*- coding: utf-8 -*- - import datetime -from openfisca_core.model_api import Variable -from openfisca_core.periods import MONTH, ETERNITY -from openfisca_core.simulation_builder import SimulationBuilder -from openfisca_core.tools import assert_near +from pytest import fixture, mark, raises import openfisca_country_template as country_template import openfisca_country_template.situation_examples from openfisca_country_template.entities import Person -from pytest import fixture, raises, mark +from openfisca_core import periods +from openfisca_core.simulation_builder import SimulationBuilder +from openfisca_core.tools import assert_near +from openfisca_core.variables import Variable # Check which date is applied whether it comes from Variable attribute (end) # or formula(s) dates. @@ -58,7 +56,7 @@ def get_message(error): class variable__no_date(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable without date." @@ -78,7 +76,7 @@ def test_variable__no_date(): class variable__strange_end_attribute(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable with dubious end attribute, no formula." end = '1989-00-00' @@ -100,7 +98,7 @@ def test_variable__strange_end_attribute(): class variable__end_attribute(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable with end attribute, no formula." end = '1989-12-31' @@ -127,7 +125,7 @@ def test_variable__end_attribute_set_input(simulation): class end_attribute__one_simple_formula(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable with end attribute, one formula without date." end = '1989-12-31' @@ -170,7 +168,7 @@ def test_dates__end_attribute__one_simple_formula(): class no_end_attribute__one_formula__strange_name(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable without end attribute, one stangely named formula." def formula_2015_toto(individu, period): @@ -187,7 +185,7 @@ def test_add__no_end_attribute__one_formula__strange_name(): class no_end_attribute__one_formula__start(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable without end attribute, one dated formula." def formula_2000_01_01(individu, period): @@ -219,7 +217,7 @@ def test_dates__no_end_attribute__one_formula__start(): class no_end_attribute__one_formula__eternity(Variable): value_type = int entity = Person - definition_period = ETERNITY # For this entity, this variable shouldn't evolve through time + definition_period = periods.DateUnit.ETERNITY # For this entity, this variable shouldn't evolve through time label = "Variable without end attribute, one dated formula." def formula_2000_01_01(individu, period): @@ -255,7 +253,7 @@ def test_call__no_end_attribute__one_formula__eternity_after(simulation): class no_end_attribute__formulas__start_formats(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable without end attribute, multiple dated formulas." def formula_2000(individu, period): @@ -307,7 +305,7 @@ def test_call__no_end_attribute__formulas__start_formats(simulation): class no_attribute__formulas__different_names__dates_overlap(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable, no end attribute, multiple dated formulas with different names but same dates." def formula_2000(individu, period): @@ -327,7 +325,7 @@ def test_add__no_attribute__formulas__different_names__dates_overlap(): class no_attribute__formulas__different_names__no_overlap(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable, no end attribute, multiple dated formulas with different names and no date overlap." def formula_2000_01_01(individu, period): @@ -356,7 +354,7 @@ def test_call__no_attribute__formulas__different_names__no_overlap(simulation): class end_attribute__one_formula__start(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable with end attribute, one dated formula." end = '2001-12-31' @@ -383,7 +381,7 @@ def test_call__end_attribute__one_formula__start(simulation): class stop_attribute_before__one_formula__start(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable with stop attribute only coming before formula start." end = '1990-01-01' @@ -400,7 +398,7 @@ def test_add__stop_attribute_before__one_formula__start(): class end_attribute_restrictive__one_formula(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable with end attribute, one dated formula and dates intervals overlap." end = '2001-01-01' @@ -427,7 +425,7 @@ def test_call__end_attribute_restrictive__one_formula(simulation): class end_attribute__formulas__different_names(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH label = "Variable with end attribute, multiple dated formulas with different names." end = '2010-12-31' @@ -468,7 +466,7 @@ def test_unexpected_attr(): class variable_with_strange_attr(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.DateUnit.MONTH unexpected = '???' with raises(ValueError): diff --git a/tests/fixtures/appclient.py b/tests/fixtures/appclient.py index a140e0f938..4c3b8f35b9 100644 --- a/tests/fixtures/appclient.py +++ b/tests/fixtures/appclient.py @@ -20,7 +20,7 @@ def test_client(tax_benefit_system): class new_variable(Variable): value_type = float entity = entities.Person - definition_period = periods.MONTH + definition_period = periods.DateUnit.MONTH label = "New variable" reference = "https://law.gov.example/new_variable" # Always use the most official source diff --git a/tests/fixtures/variables.py b/tests/fixtures/variables.py index cd0d9b70ce..6de67a5fcb 100644 --- a/tests/fixtures/variables.py +++ b/tests/fixtures/variables.py @@ -3,7 +3,7 @@ class TestVariable(Variable): - definition_period = periods.ETERNITY + definition_period = periods.DateUnit.ETERNITY value_type = float def __init__(self, entity): diff --git a/tests/web_api/basic_case/__init__.py b/tests/web_api/basic_case/__init__.py index 4114c06467..d68f4ea799 100644 --- a/tests/web_api/basic_case/__init__.py +++ b/tests/web_api/basic_case/__init__.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import pkg_resources -from openfisca_web_api.app import create_app + from openfisca_core.scripts import build_tax_benefit_system +from openfisca_web_api.app import create_app TEST_COUNTRY_PACKAGE_NAME = 'openfisca_country_template' distribution = pkg_resources.get_distribution(TEST_COUNTRY_PACKAGE_NAME) diff --git a/tests/web_api/case_with_extension/test_extensions.py b/tests/web_api/case_with_extension/test_extensions.py index 4da94bf45c..bfc1003f29 100644 --- a/tests/web_api/case_with_extension/test_extensions.py +++ b/tests/web_api/case_with_extension/test_extensions.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from http.client import OK + from openfisca_core.scripts import build_tax_benefit_system from openfisca_web_api.app import create_app - TEST_COUNTRY_PACKAGE_NAME = 'openfisca_country_template' TEST_EXTENSION_PACKAGE_NAMES = ['openfisca_extension_template'] diff --git a/tests/web_api/case_with_reform/test_reforms.py b/tests/web_api/case_with_reform/test_reforms.py index 5037a4b395..b0e46ce753 100644 --- a/tests/web_api/case_with_reform/test_reforms.py +++ b/tests/web_api/case_with_reform/test_reforms.py @@ -1,4 +1,5 @@ import http + import pytest from openfisca_core import scripts diff --git a/tests/web_api/loader/test_parameters.py b/tests/web_api/loader/test_parameters.py index e17472a9d6..863d0fd44d 100644 --- a/tests/web_api/loader/test_parameters.py +++ b/tests/web_api/loader/test_parameters.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- from openfisca_core.parameters import Scale - -from openfisca_web_api.loader.parameters import build_api_scale, build_api_parameter +from openfisca_web_api.loader.parameters import ( + build_api_parameter, + build_api_scale, + ) def test_build_rate_scale(): diff --git a/tests/web_api/test_calculate.py b/tests/web_api/test_calculate.py index 0fcc67f0b7..7df3dccd4a 100644 --- a/tests/web_api/test_calculate.py +++ b/tests/web_api/test_calculate.py @@ -1,8 +1,9 @@ import copy -import dpath.util import json -from http import client import os +from http import client + +import dpath.util import pytest from openfisca_country_template.situation_examples import couple @@ -41,8 +42,8 @@ def check_response(client, data, expected_error_code, path_to_check, content_to_ ('{"persons": {"bob": {}}, "households": {"household": {"parents": ["unexpected_person_id"]}}}', client.BAD_REQUEST, 'households/household/parents', 'has not been declared in persons',), ('{"persons": {"bob": {}}, "households": {"household": {"parents": ["bob", "bob"]}}}', client.BAD_REQUEST, 'households/household/parents', 'has been declared more than once',), ('{"persons": {"bob": {}}, "households": {"household": {"parents": ["bob", {}]}}}', client.BAD_REQUEST, 'households/household/parents/1', 'Invalid type',), - ('{"persons": {"bob": {"salary": {"invalid period": 2000 }}}}', client.BAD_REQUEST, 'persons/bob/salary', 'Expected a period',), - ('{"persons": {"bob": {"salary": {"invalid period": null }}}}', client.BAD_REQUEST, 'persons/bob/salary', 'Expected a period',), + ('{"persons": {"bob": {"salary": {"invalid period": 2000 }}}}', client.BAD_REQUEST, 'persons/bob/salary', 'is not a valid period',), + ('{"persons": {"bob": {"salary": {"invalid period": null }}}}', client.BAD_REQUEST, 'persons/bob/salary', 'is not a valid period',), ('{"persons": {"bob": {"basic_income": {"2017": 2000 }}}, "households": {"household": {"parents": ["bob"]}}}', client.BAD_REQUEST, 'persons/bob/basic_income/2017', '"basic_income" can only be set for one month',), ('{"persons": {"bob": {"salary": {"ETERNITY": 2000 }}}, "households": {"household": {"parents": ["bob"]}}}', client.BAD_REQUEST, 'persons/bob/salary/ETERNITY', 'salary is only defined for months',), ('{"persons": {"alice": {}, "bob": {}, "charlie": {}}, "households": {"_": {"parents": ["alice", "bob", "charlie"]}}}', client.BAD_REQUEST, 'households/_/parents', 'at most 2 parents in a household',), @@ -268,7 +269,7 @@ def test_encoding_period_id(test_client): response_json = json.loads(response.data.decode('utf-8')) # In Python 3, there is no encoding issue. - if "Expected a period" not in str(response.data): + if "is not a valid period" not in str(response.data): message = "'à' is not a valid ASCII value." text = response_json['error'] assert message in text diff --git a/tests/web_api/test_entities.py b/tests/web_api/test_entities.py index 6f8153ed37..d2de5fbb67 100644 --- a/tests/web_api/test_entities.py +++ b/tests/web_api/test_entities.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -from http import client import json +from http import client from openfisca_country_template import entities - # /entities diff --git a/tests/web_api/test_helpers.py b/tests/web_api/test_helpers.py index cb049a0822..94e377db73 100644 --- a/tests/web_api/test_helpers.py +++ b/tests/web_api/test_helpers.py @@ -1,9 +1,7 @@ import os -from openfisca_web_api.loader import parameters - from openfisca_core.parameters import load_parameter_file - +from openfisca_web_api.loader import parameters dir_path = os.path.join(os.path.dirname(__file__), 'assets') diff --git a/tests/web_api/test_parameters.py b/tests/web_api/test_parameters.py index 8f65cca9af..efcb8d4a1d 100644 --- a/tests/web_api/test_parameters.py +++ b/tests/web_api/test_parameters.py @@ -1,8 +1,8 @@ -from http import client import json -import pytest import re +from http import client +import pytest # /parameters diff --git a/tests/web_api/test_spec.py b/tests/web_api/test_spec.py index 4c34532b73..7a781aed87 100644 --- a/tests/web_api/test_spec.py +++ b/tests/web_api/test_spec.py @@ -1,9 +1,9 @@ -import dpath.util import json from http import client -from openapi_spec_validator import openapi_v3_spec_validator +import dpath.util import pytest +from openapi_spec_validator import openapi_v3_spec_validator def assert_items_equal(x, y): diff --git a/tests/web_api/test_trace.py b/tests/web_api/test_trace.py index 3f0b3aa947..09b7e117fc 100644 --- a/tests/web_api/test_trace.py +++ b/tests/web_api/test_trace.py @@ -1,9 +1,10 @@ import copy -import dpath.util -from http import client import json +from http import client + +import dpath.util -from openfisca_country_template.situation_examples import single, couple +from openfisca_country_template.situation_examples import couple, single def assert_items_equal(x, y): diff --git a/tests/web_api/test_variables.py b/tests/web_api/test_variables.py index 4581608aa8..bbf5f6c99d 100644 --- a/tests/web_api/test_variables.py +++ b/tests/web_api/test_variables.py @@ -1,7 +1,8 @@ -from http import client import json -import pytest import re +from http import client + +import pytest def assert_items_equal(x, y):