From 9872a39e4eeb44f20f074330b8cbc0fc6c13c029 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 31 Jul 2022 14:50:28 +0200 Subject: [PATCH 01/93] Fix flake8/pycodestyle dependency error --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 2543121ac8..38837bacb1 100644 --- a/setup.py +++ b/setup.py @@ -38,18 +38,18 @@ ] api_requirements = [ - 'markupsafe == 2.0.1', # While flask revision < 2 - 'flask == 1.1.4', + 'markupsafe >= 2.0.1, < 2.1.0', + 'flask >= 1.1.4, < 2.0.0', 'flask-cors == 3.0.10', 'gunicorn >= 20.0.0, < 21.0.0', - 'werkzeug >= 1.0.0, < 2.0.0', + 'werkzeug >= 1.0.1, < 2.0.0', ] dev_requirements = [ 'autopep8 >= 1.4.0, < 1.6.0', 'coverage == 6.0.2', 'darglint == 1.8.0', - 'flake8 >= 4.0.0, < 4.1.0', + 'flake8 >= 3.9.0, < 4.0.0', 'flake8-bugbear >= 19.3.0, < 20.0.0', 'flake8-docstrings == 1.6.0', 'flake8-print >= 3.1.0, < 4.0.0', From 35e02c5ca877be1617ac567651a5ee96af170986 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 28 Jul 2022 18:36:11 +0200 Subject: [PATCH 02/93] Add periods to doctests path --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 467e3ede59..31815f0da2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,7 @@ extend-ignore = D hang-closing = true ignore = E128,E251,F403,F405,E501,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 rst-directives = attribute, deprecated, seealso, versionadded, versionchanged rst-roles = any, attr, class, exc, func, meth, mod, obj strictness = short From 8ba7fe1ca7bea87683d0c35d4aaeedc888221baf Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 28 Jul 2022 18:37:47 +0200 Subject: [PATCH 03/93] Ignore mypy periods' tests errors --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 31815f0da2..2144a48610 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,5 +54,8 @@ ignore_errors = True [mypy-openfisca_core.holders.tests.*] ignore_errors = True +[mypy-openfisca_core.periods.tests.*] +ignore_errors = True + [mypy-openfisca_core.scripts.*] ignore_errors = True From 496951f7c09dd9f0cd6c6412d9562bb3eb630200 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 28 Jul 2022 19:53:11 +0200 Subject: [PATCH 04/93] Fix instants' docstrings --- openfisca_core/periods/instant_.py | 242 ++++++++++++----------------- 1 file changed, 102 insertions(+), 140 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index ad559e5e53..48c3a1d995 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import calendar import datetime @@ -5,32 +7,83 @@ class Instant(tuple): + """An instant in time (year, month, day). + + An :class:`.Instant` represents the most atomic and indivisible + legislation's time unit. + + Current implementation considers this unit to be a day, so + :obj:`instants <.Instant>` 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)) + + >>> repr(Instant) + "" + + >>> repr(instant) + 'Instant((2021, 9, 13))' + + >>> str(instant) + '2021-09-13' + + >>> dict([(instant, (2021, 9, 13))]) + {Instant((2021, 9, 13)): (2021, 9, 13)} + + >>> list(instant) + [2021, 9, 13] + + >>> instant[0] + 2021 + + >>> instant[0] in instant + True + + >>> len(instant) + 3 + + >>> instant == (2021, 9, 13) + True + + >>> instant != (2021, 9, 13) + False + + >>> instant > (2020, 9, 13) + True + + >>> instant < (2020, 9, 13) + False + + >>> instant >= (2020, 9, 13) + True + + >>> instant <= (2020, 9, 13) + False + + >>> instant.year + 2021 + + >>> instant.month + 9 + + >>> instant.day + 13 + + >>> instant.date + datetime.date(2021, 9, 13) + + >>> year, month, day = instant + + """ 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() @@ -38,16 +91,6 @@ def __str__(self): @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) @@ -55,113 +98,42 @@ def date(self): @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): + """Increments/decrements the given instant with offset units. + + Args: + offset: How much of ``unit`` to offset. + unit: What to offset + + Returns: + :obj:`.Instant`: A new :obj:`.Instant` in time. + + Raises: + :exc:`AssertionError`: When ``unit`` is not a date unit. + :exc:`AssertionError`: When ``offset`` is not either ``first-of``, + ``last-of``, or any :obj:`int`. + + Examples: + >>> Instant((2020, 12, 31)).offset("first-of", "month") + Instant((2020, 12, 1)) + + >>> Instant((2020, 1, 1)).offset("last-of", "year") + Instant((2020, 12, 31)) + + >>> Instant((2020, 1, 1)).offset(1, "year") + Instant((2021, 1, 1)) + + >>> Instant((2020, 1, 1)).offset(-3, "day") + Instant((2019, 12, 29)) + """ - 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': @@ -220,14 +192,4 @@ def offset(self, offset, unit): @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] From fbafc60780fa37ed750289c5210de4c685613d33 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 28 Jul 2022 20:56:36 +0200 Subject: [PATCH 05/93] Fix periods' doctests --- openfisca_core/periods/period_.py | 614 ++++++++++++++++++------------ setup.py | 2 +- 2 files changed, 365 insertions(+), 251 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 7de0459bdf..9f2d9c29f9 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -7,54 +7,158 @@ class Period(tuple): - """ - Toolbox to handle date intervals. + """Toolbox to handle date intervals. + + A :class:`.Period` is a triple (``unit``, ``start``, ``size``). + + Attributes: + unit (:obj:`.DateUnit`): + Either an :meth:`~DateUnit.isoformat` unit (``day``, ``month``, + ``year``), an :meth:`~DateUnit.isocalendar` one (``week_day``, + ``week``, ``year``), or :obj:`~DateUnit.ETERNITY`. + start (:obj:`.Instant`): + The "instant" the :obj:`.Period` starts at. + size (:obj:`int`): + The amount of ``unit``, starting at ``start``, at least ``1``. + + Args: + fragments (tuple(.DateUnit, .Instant, int)): + The ``unit``, ``start``, and ``size``, accordingly. + + Examples: + >>> instant = Instant((2021, 9, 1)) + >>> period = Period(("year", instant, 3)) + + >>> repr(Period) + "" + + >>> repr(period) + "Period(('year', Instant((2021, 9, 1)), 3))" + + >>> str(period) + 'year:2021-09:3' + + >>> dict([period, instant]) + Traceback (most recent call last): + ValueError: dictionary update sequence element #0 has length 3; 2 is required + + >>> list(period) + ['year', Instant((2021, 9, 1)), 3] + + >>> period[0] + 'year' + + >>> period[0] in period + True + + >>> len(period) + 3 + + >>> period == Period(("year", instant, 3)) + True + + >>> period != Period(("year", instant, 3)) + False + + >>> period > Period(("year", instant, 3)) + False + + >>> period < Period(("year", instant, 3)) + False + + >>> period >= Period(("year", instant, 3)) + True - 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. + >>> period <= Period(("year", instant, 3)) + True + >>> period.date + Traceback (most recent call last): + AssertionError: "date" is undefined for a period of size > 1 + + >>> Period(("year", instant, 1)).date + datetime.date(2021, 9, 1) + + >>> period.days + 1096 + + >>> period.size + 3 + + >>> period.size_in_months + 36 + + >>> period.size_in_days + 1096 + + >>> period.start + Instant((2021, 9, 1)) + + >>> period.stop + Instant((2024, 8, 31)) + + >>> period.unit + 'year' + + >>> period.last_3_months + Period(('month', Instant((2021, 6, 1)), 3)) + + >>> period.last_month + Period(('month', Instant((2021, 8, 1)), 1)) + + >>> period.last_year + Period(('year', Instant((2020, 1, 1)), 1)) + + >>> period.n_2 + Period(('year', Instant((2019, 1, 1)), 1)) + + >>> period.this_year + Period(('year', Instant((2021, 1, 1)), 1)) + + >>> period.first_month + Period(('month', Instant((2021, 9, 1)), 1)) + + >>> period.first_day + Period(('day', Instant((2021, 9, 1)), 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' + """Transform period to a string. + + Examples: + >>> str(Period(("year", Instant((2021, 1, 1)), 1))) + '2021' + + >>> str(Period(("year", Instant((2021, 2, 1)), 1))) + 'year:2021-02' + + >>> str(Period(("month", Instant((2021, 2, 1)), 1))) + '2021-02' + + >>> str(Period(("year", Instant((2021, 1, 1)), 2))) + 'year:2021:2' + + >>> str(Period(("month", Instant((2021, 1, 1)), 2))) + 'month:2021-01:2' + + >>> str(Period(("month", Instant((2021, 1, 1)), 12))) + '2021' + + >>> str(Period(("year", Instant((2021, 3, 1)), 2))) + 'year:2021-03:2' + + >>> str(Period(("month", Instant((2021, 3, 1)), 2))) + 'month:2021-03:2' + + >>> str(Period(("month", Instant((2021, 3, 1)), 12))) + 'year:2021-03' + """ unit, start_instant, size = self @@ -93,30 +197,8 @@ def date(self): @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 - """ + """Count the number of days in period.""" + return (self.stop.date - self.start.date).days + 1 def intersection(self, start, stop): @@ -160,17 +242,19 @@ def intersection(self, start, stop): )) def get_subperiods(self, unit): - """ - Return the list of all the periods of unit ``unit`` contained in self. + """Return the list of all the periods of unit ``unit`` contained in self. Examples: + >>> period = Period(("year", Instant((2021, 1, 1)), 1)) + >>> period.get_subperiods("month") + [Period(('month', Instant((2021, 1, 1)), 1)), ...2021, 12, 1)), 1))] - >>> period('2017').get_subperiods(MONTH) - >>> [period('2017-01'), period('2017-02'), ... period('2017-12')] + >>> period = Period(("year", Instant((2021, 1, 1)), 2)) + >>> period.get_subperiods("year") + [Period(('year', Instant((2021, 1, 1)), 1)), ...((2022, 1, 1)), 1))] - >>> 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)) @@ -184,168 +268,200 @@ def get_subperiods(self, unit): 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. + + Examples: + >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1) + Period(('day', Instant((2021, 1, 2)), 365)) + + >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "day") + Period(('day', Instant((2021, 1, 2)), 365)) + + >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "month") + Period(('day', Instant((2021, 2, 1)), 365)) + + >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "year") + Period(('day', Instant((2022, 1, 1)), 365)) + + >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(1) + Period(('month', Instant((2021, 2, 1)), 12)) + + >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(1, "day") + Period(('month', Instant((2021, 1, 2)), 12)) + + >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(1, "month") + Period(('month', Instant((2021, 2, 1)), 12)) + + >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(1, "year") + Period(('month', Instant((2022, 1, 1)), 12)) + + >>> Period(("year", Instant((2021, 1, 1)), 1)).offset(1) + Period(('year', Instant((2022, 1, 1)), 1)) + + >>> Period(("year", Instant((2021, 1, 1)), 1)).offset(1, "day") + Period(('year', Instant((2021, 1, 2)), 1)) + + >>> Period(("year", Instant((2021, 1, 1)), 1)).offset(1, "month") + Period(('year', Instant((2021, 2, 1)), 1)) + + >>> Period(("year", Instant((2021, 1, 1)), 1)).offset(1, "year") + Period(('year', Instant((2022, 1, 1)), 1)) + + >>> Period(("day", Instant((2011, 2, 28)), 1)).offset(1) + Period(('day', Instant((2011, 3, 1)), 1)) + + >>> Period(("month", Instant((2011, 2, 28)), 1)).offset(1) + Period(('month', Instant((2011, 3, 28)), 1)) + + >>> Period(("year", Instant((2011, 2, 28)), 1)).offset(1) + Period(('year', Instant((2012, 2, 28)), 1)) + + >>> Period(("day", Instant((2011, 3, 1)), 1)).offset(-1) + Period(('day', Instant((2011, 2, 28)), 1)) + + >>> Period(("month", Instant((2011, 3, 1)), 1)).offset(-1) + Period(('month', Instant((2011, 2, 1)), 1)) + + >>> Period(("year", Instant((2011, 3, 1)), 1)).offset(-1) + Period(('year', Instant((2010, 3, 1)), 1)) + + >>> Period(("day", Instant((2014, 1, 30)), 1)).offset(3) + Period(('day', Instant((2014, 2, 2)), 1)) + + >>> Period(("month", Instant((2014, 1, 30)), 1)).offset(3) + Period(('month', Instant((2014, 4, 30)), 1)) + + >>> Period(("year", Instant((2014, 1, 30)), 1)).offset(3) + Period(('year', Instant((2017, 1, 30)), 1)) + + >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(-3) + Period(('day', Instant((2020, 12, 29)), 365)) + + >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(-3) + Period(('month', Instant((2020, 10, 1)), 12)) + + >>> Period(("year", Instant((2014, 1, 1)), 1)).offset(-3) + Period(('year', Instant((2011, 1, 1)), 1)) + + >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("first-of", "month") + Period(('day', Instant((2014, 2, 1)), 1)) + + >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("first-of", "year") + Period(('day', Instant((2014, 1, 1)), 1)) + + >>> Period(("day", Instant((2014, 2, 3)), 4)).offset("first-of", "month") + Period(('day', Instant((2014, 2, 1)), 4)) + + >>> Period(("day", Instant((2014, 2, 3)), 4)).offset("first-of", "year") + Period(('day', Instant((2014, 1, 1)), 4)) + + >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("first-of") + Period(('month', Instant((2014, 2, 1)), 1)) + + >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("first-of", "month") + Period(('month', Instant((2014, 2, 1)), 1)) + + >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("first-of", "year") + Period(('month', Instant((2014, 1, 1)), 1)) + + >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("first-of") + Period(('month', Instant((2014, 2, 1)), 4)) + + >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("first-of", "month") + Period(('month', Instant((2014, 2, 1)), 4)) + + >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("first-of", "year") + Period(('month', Instant((2014, 1, 1)), 4)) + + >>> Period(("year", Instant((2014, 1, 30)), 1)).offset("first-of") + Period(('year', Instant((2014, 1, 1)), 1)) + + >>> Period(("year", Instant((2014, 1, 30)), 1)).offset("first-of", "month") + Period(('year', Instant((2014, 1, 1)), 1)) + + >>> Period(("year", Instant((2014, 1, 30)), 1)).offset("first-of", "year") + Period(('year', Instant((2014, 1, 1)), 1)) + + >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("first-of") + Period(('year', Instant((2014, 1, 1)), 1)) + + >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("first-of", "month") + Period(('year', Instant((2014, 2, 1)), 1)) + + >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("first-of", "year") + Period(('year', Instant((2014, 1, 1)), 1)) + + >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("last-of", "month") + Period(('day', Instant((2014, 2, 28)), 1)) + + >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("last-of", "year") + Period(('day', Instant((2014, 12, 31)), 1)) + + >>> Period(("day", Instant((2014, 2, 3)), 4)).offset("last-of", "month") + Period(('day', Instant((2014, 2, 28)), 4)) + + >>> Period(("day", Instant((2014, 2, 3)), 4)).offset("last-of", "year") + Period(('day', Instant((2014, 12, 31)), 4)) + + >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("last-of") + Period(('month', Instant((2014, 2, 28)), 1)) + + >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("last-of", "month") + Period(('month', Instant((2014, 2, 28)), 1)) + + >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("last-of", "year") + Period(('month', Instant((2014, 12, 31)), 1)) + + >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of") + Period(('month', Instant((2014, 2, 28)), 4)) + + >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of", "month") + Period(('month', Instant((2014, 2, 28)), 4)) + + >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of", "year") + Period(('month', Instant((2014, 12, 31)), 4)) + + >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of") + Period(('year', Instant((2014, 12, 31)), 1)) + + >>> Period(("year", Instant((2014, 1, 1)), 1)).offset("last-of", "month") + Period(('year', Instant((2014, 1, 31)), 1)) + + >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of", "year") + Period(('year', Instant((2014, 12, 31)), 1)) + + >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of") + Period(('year', Instant((2014, 12, 31)), 1)) + + >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of", "month") + Period(('year', Instant((2014, 2, 28)), 1)) + + >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of", "year") + Period(('year', Instant((2014, 12, 31)), 1)) + """ - 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)``. + """ - 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. + """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. + """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): @@ -354,14 +470,8 @@ def size_in_months(self): @property def size_in_days(self): - """ - Return the size of the period in days. + """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: @@ -374,40 +484,44 @@ def size_in_days(self): @property def start(self) -> Instant: - """ - Return the first day of the period as an Instant instance. + """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. + + Examples: + >>> Period(("year", Instant((2022, 1, 1)), 1)).stop + Instant((2022, 12, 31)) + + >>> Period(("month", Instant((2022, 1, 1)), 12)).stop + Instant((2022, 12, 31)) + + >>> Period(("day", Instant((2022, 1, 1)), 365)).stop + Instant((2022, 12, 31)) + + >>> Period(("year", Instant((2012, 2, 29)), 1)).stop + Instant((2013, 2, 28)) + + >>> Period(("month", Instant((2012, 2, 29)), 1)).stop + Instant((2012, 3, 28)) + + >>> Period(("day", Instant((2012, 2, 29)), 1)).stop + Instant((2012, 2, 29)) + + >>> Period(("year", Instant((2012, 2, 29)), 2)).stop + Instant((2014, 2, 28)) + + >>> Period(("month", Instant((2012, 2, 29)), 2)).stop + Instant((2012, 4, 28)) + + >>> Period(("day", Instant((2012, 2, 29)), 2)).stop + Instant((2012, 3, 1)) + """ - 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: diff --git a/setup.py b/setup.py index 38837bacb1..68d0b0f029 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ 'flake8-rst-docstrings == 0.2.3', 'mypy == 0.910', 'openapi-spec-validator >= 0.3.0', - 'pycodestyle >= 2.8.0, < 2.9.0', + 'pycodestyle >= 2.7.0, < 2.8.0', 'pylint == 2.10.2', 'xdoctest >= 1.0.0, < 2.0.0', ] + api_requirements From 1b8cfecc37e42c19b98e33587cbafaebd006eaef Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 28 Jul 2022 21:44:54 +0200 Subject: [PATCH 06/93] Fix periods' helpers doctests --- openfisca_core/periods/config.py | 21 +- openfisca_core/periods/helpers.py | 348 ++++++++++++++++++++--------- openfisca_core/periods/instant_.py | 42 ++-- openfisca_core/periods/period_.py | 107 +++------ openfisca_core/types/_domain.py | 13 ++ 5 files changed, 327 insertions(+), 204 deletions(-) diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/config.py index 6e0c698098..657831d527 100644 --- a/openfisca_core/periods/config.py +++ b/openfisca_core/periods/config.py @@ -1,15 +1,18 @@ +from __future__ import annotations + +from typing import Dict, Pattern + import re -import typing -DAY = 'day' -MONTH = 'month' -YEAR = 'year' -ETERNITY = 'eternity' +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]))?$") +INSTANT_PATTERN: 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]))?)?$') +date_by_instant_cache: Dict = {} +str_by_instant_cache: Dict = {} +year_or_month_or_day_re: Pattern = 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/helpers.py b/openfisca_core/periods/helpers.py index e4f93e4edb..994e126b40 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -1,186 +1,302 @@ -from typing import Dict +from __future__ import annotations + +from typing import Any, Dict, NoReturn, Optional import datetime import os +from openfisca_core import types + 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). +def instant(value: Any) -> Optional[types.Instant]: + """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: + ValueError: When the arguments were invalid, like "2021-32-13". + + Examples: + >>> instant(datetime.date(2021, 9, 16)) + Instant((2021, 9, 16)) - >>> 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(Instant((2021, 9, 16))) + Instant((2021, 9, 16)) + + >>> instant(Period(("year", Instant((2021, 9, 16)), 1))) + Instant((2021, 9, 16)) + + >>> instant("2021") + Instant((2021, 1, 1)) + + >>> instant(2021) + Instant((2021, 1, 1)) + + >>> instant((2021, 9)) + Instant((2021, 9, 1)) - >>> instant(None) """ - if instant is None: + + if value 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)) + + if isinstance(value, Instant): + return value + + if isinstance(value, str): + if not config.INSTANT_PATTERN.match(value): + raise ValueError(f"'{value}' is not a valid instant. Instants are described using the 'YYYY-MM-DD' format, for instance '2015-06-15'.") + instant = Instant( int(fragment) - for fragment in instant.split('-', 2)[:3] + for fragment in value.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 + + elif isinstance(value, datetime.date): + instant = Instant((value.year, value.month, value.day)) + + elif isinstance(value, int): + instant = (value,) + + elif isinstance(value, list): + assert 1 <= len(value) <= 3 + instant = tuple(value) + + elif isinstance(value, Period): + instant = value.start + else: - assert isinstance(instant, tuple), instant - assert 1 <= len(instant) <= 3 + assert isinstance(value, tuple), value + assert 1 <= len(value) <= 3 + instant = 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 instant_date(instant): +def instant_date(instant: Optional[types.Instant]) -> Optional[datetime.date]: + """Returns the date representation of an ``Instant``. + + Args: + instant (:obj:`.Instant`, optional): + An ``instant`` to get the date from. + + Returns: + None: When ``instant`` is None. + datetime.date: Otherwise. + + Examples: + >>> instant_date(Instant((2021, 1, 1))) + datetime.date(2021, 1, 1) + + """ + 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). +def period(value: Any) -> types.Period: + """Build a new period, aka a triple (unit, start_instant, size). + + Args: + value: A ``period-like`` object. + + Returns: + :obj:`.Period`: A period. - >>> period('2014') - Period((YEAR, Instant((2014, 1, 1)), 1)) - >>> period('year:2014') - Period((YEAR, Instant((2014, 1, 1)), 1)) + Raises: + :exc:`ValueError`: When the arguments were invalid, like "2021-32-13". - >>> 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)) + Examples: + >>> period(Period(("year", Instant((2021, 1, 1)), 1))) + Period(('year', Instant((2021, 1, 1)), 1)) + + >>> period(Instant((2021, 1, 1))) + Period(('day', Instant((2021, 1, 1)), 1)) + + >>> period("eternity") + Period(('eternity', Instant((1, 1, 1)), inf)) + + >>> period(2021) + Period(('year', Instant((2021, 1, 1)), 1)) + + >>> period("2014") + Period(('year', Instant((2014, 1, 1)), 1)) + + >>> period("year:2014") + Period(('year', Instant((2014, 1, 1)), 1)) + + >>> period("month:2014-2") + Period(('month', Instant((2014, 2, 1)), 1)) + + >>> period("year:2014-2") + Period(('year', Instant((2014, 2, 1)), 1)) + + >>> period("day:2014-2-2") + Period(('day', Instant((2014, 2, 2)), 1)) + + >>> period("day:2014-2-2:3") + Period(('day', Instant((2014, 2, 2)), 3)) - >>> 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"))) - 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) + _raise_error(value) + + # Try to parse as a simple period + period = _parse_simple_period(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 + # Complex periods must have a ':' in their strings if ":" not in value: - raise_error(value) + _raise_error(value) components = value.split(':') - # left-most component must be a valid unit + # Left-most component must be a valid unit unit = components[0] + if unit not in (config.DAY, config.MONTH, config.YEAR): - raise_error(value) + _raise_error(value) + + # Middle component must be a valid iso period + base_period = _parse_simple_period(components[1]) - # middle component must be a valid iso period - base_period = parse_simple_period(components[1]) if not base_period: - raise_error(value) + _raise_error(value) - # period like year:2015-03 have a size of 1 + # Periods like year:2015-03 have a size of 1 if len(components) == 2: size = 1 - # if provided, make sure the size is an integer + + # 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 + _raise_error(value) + + # If there are more than 2 ":" in the string, the period is invalid else: - raise_error(value) + _raise_error(value) - # reject ambiguous period such as month:2014 + # Reject ambiguous periods such as month:2014 if unit_weight(base_period.unit) > unit_weight(unit): - raise_error(value) + _raise_error(value) return Period((unit, base_period.start, size)) -def key_period_size(period): +def _parse_simple_period(value: str) -> Optional[types.Period]: + """Parse simple periods respecting the ISO format. + + Such as "2012" or "2015-03". + + Examples: + >>> _parse_simple_period("2022") + Period(('year', Instant((2022, 1, 1)), 1)) + + >>> _parse_simple_period("2022-02") + Period(('month', Instant((2022, 2, 1)), 1)) + + >>> _parse_simple_period("2022-02-13") + Period(('day', Instant((2022, 2, 13)), 1)) + """ - 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 + 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)) + - >>> key_period_size(period('2014')) - '2_1' - >>> key_period_size(period('2013')) - '2_1' - >>> key_period_size(period('2014-01')) - '1_1' +def _raise_error(value: str) -> NoReturn: + """Raise an error. + + Examples: + >>> _raise_error("Oi mate!") + Traceback (most recent call last): + ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: 'Oi mate!'. + + """ + + 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) + + +def key_period_size(period: types.Period) -> str: + """Define a key in order to sort periods by length. + + It uses two aspects: first, ``unit``, then, ``size``. + + Args: + period: An :mod:`.openfisca_core` :obj:`.Period`. + + Returns: + :obj:`str`: A string. + + Examples: + >>> instant = Instant((2021, 9, 14)) + + >>> period = Period(("day", instant, 1)) + >>> key_period_size(period) + '100_1' + + >>> period = Period(("year", instant, 3)) + >>> key_period_size(period) + '300_3' """ @@ -190,6 +306,14 @@ def key_period_size(period): def unit_weights() -> Dict[str, int]: + """Assign weights to date units. + + Examples: + >>> unit_weights() + {'day': 100, ...} + + """ + return { config.DAY: 100, config.MONTH: 200, @@ -199,4 +323,12 @@ def unit_weights() -> Dict[str, int]: def unit_weight(unit: str) -> int: + """Retrieves a specific date unit weight. + + Examples: + >>> unit_weight("day") + 100 + + """ + return unit_weights()[unit] diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 48c3a1d995..63cc63636a 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,8 +1,12 @@ from __future__ import annotations +from typing import Union + import calendar import datetime +from openfisca_core import types + from . import config @@ -16,7 +20,7 @@ class Instant(tuple): :obj:`instants <.Instant>` can be thought of as "day dates". Args: - tuple(tuple(int, int, int)): + (tuple(tuple(int, int, int))): The ``year``, ``month``, and ``day``, accordingly. Examples: @@ -80,31 +84,39 @@ class Instant(tuple): """ - def __repr__(self): - return '{}({})'.format(self.__class__.__name__, super(Instant, self).__repr__()) + def __repr__(self) -> str: + return f"{self.__class__.__name__}({super(Instant, self).__repr__()})" - def __str__(self): + def __str__(self) -> str: 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): - 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 + def year(self) -> int: + return self[0] + + @property + def month(self) -> int: + return self[1] @property - def day(self): + def day(self) -> int: return self[2] @property - def month(self): - return self[1] + def date(self) -> datetime.date: + 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) - def offset(self, offset, unit): + return instant_date + + def offset(self, offset: Union[str, int], unit: str) -> types.Instant: """Increments/decrements the given instant with offset units. Args: @@ -189,7 +201,3 @@ def offset(self, offset, unit): day = month_last_day return self.__class__((year, month, day)) - - @property - def year(self): - return self[0] diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 9f2d9c29f9..464ea51020 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,6 +1,11 @@ from __future__ import annotations +from typing import Optional, Sequence, Union + import calendar +import datetime + +from openfisca_core import types from . import config, helpers from .instant_ import Instant @@ -12,17 +17,15 @@ class Period(tuple): A :class:`.Period` is a triple (``unit``, ``start``, ``size``). Attributes: - unit (:obj:`.DateUnit`): - Either an :meth:`~DateUnit.isoformat` unit (``day``, ``month``, - ``year``), an :meth:`~DateUnit.isocalendar` one (``week_day``, - ``week``, ``year``), or :obj:`~DateUnit.ETERNITY`. + unit (:obj:`str`): + Either ``year``, ``month``, ``day`` or ``eternity``. start (:obj:`.Instant`): The "instant" the :obj:`.Period` starts at. size (:obj:`int`): The amount of ``unit``, starting at ``start``, at least ``1``. Args: - fragments (tuple(.DateUnit, .Instant, int)): + (tuple(tuple(str, .Instant, int))): The ``unit``, ``start``, and ``size``, accordingly. Examples: @@ -125,10 +128,10 @@ class Period(tuple): """ - def __repr__(self): - return '{}({})'.format(self.__class__.__name__, super(Period, self).__repr__()) + def __repr__(self) -> str: + return f"{self.__class__.__name__}({super(Period, self).__repr__()})" - def __str__(self): + def __str__(self) -> str: """Transform period to a string. Examples: @@ -191,57 +194,17 @@ def __str__(self): return '{}:{}-{:02d}:{}'.format(unit, year, month, size) @property - def date(self): + def date(self) -> datetime.date: assert self.size == 1, '"date" is undefined for a period of size > 1: {}'.format(self) return self.start.date @property - def days(self): + def days(self) -> int: """Count the number of days in period.""" 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): + def get_subperiods(self, unit: str) -> Sequence[types.Period]: """Return the list of all the periods of unit ``unit`` contained in self. Examples: @@ -267,7 +230,11 @@ def get_subperiods(self, unit): 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): + def offset( + self, + offset: Union[str, int], + unit: Optional[str] = None, + ) -> types.Period: """Increment (or decrement) the given period with offset units. Examples: @@ -443,7 +410,7 @@ def offset(self, offset, unit = None): 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: + def contains(self, other: types.Period) -> bool: """Returns ``True`` if the period contains ``other``. For instance, ``period(2015)`` contains ``period(2015-01)``. @@ -453,13 +420,13 @@ def contains(self, other: Period) -> bool: return self.start <= other.start and self.stop >= other.stop @property - def size(self): + def size(self) -> int: """Return the size of the period.""" return self[2] @property - def size_in_months(self): + def size_in_months(self) -> int: """Return the size of the period in months.""" if (self[0] == config.MONTH): @@ -469,7 +436,7 @@ def size_in_months(self): raise ValueError("Cannot calculate number of months in {0}".format(self[0])) @property - def size_in_days(self): + def size_in_days(self) -> int: """Return the size of the period in days.""" unit, instant, length = self @@ -483,13 +450,13 @@ def size_in_days(self): raise ValueError("Cannot calculate number of days in {0}".format(unit)) @property - def start(self) -> Instant: + def start(self) -> types.Instant: """Return the first day of the period as an Instant instance.""" return self[1] @property - def stop(self) -> Instant: + def stop(self) -> types.Instant: """Return the last day of the period as an Instant instance. Examples: @@ -570,34 +537,34 @@ def unit(self) -> str: # Reference periods @property - def last_month(self) -> Period: + def last_month(self) -> types.Period: return self.first_month.offset(-1) @property - def last_3_months(self) -> Period: - start: Instant = self.first_month.start + def last_3_months(self) -> types.Period: + start: types.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) + def last_year(self) -> types.Period: + start: types.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) + def n_2(self) -> types.Period: + start: types.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) + def this_year(self) -> types.Period: + start: types.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) + def first_month(self) -> types.Period: + start: types.Instant = self.start.offset("first-of", config.MONTH) return self.__class__((config.MONTH, start, 1)) @property - def first_day(self) -> Period: + def first_day(self) -> types.Period: return self.__class__((config.DAY, self.start, 1)) diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types/_domain.py index 643f27964f..3c64682d08 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types/_domain.py @@ -58,6 +58,10 @@ def get_memory_usage(self) -> Any: class Instant(Protocol): """Instant protocol.""" + @abc.abstractmethod + def offset(self, offset: Any, unit: Any) -> Any: + """Abstract method.""" + @typing_extensions.runtime_checkable class ParameterNodeAtInstant(Protocol): @@ -80,11 +84,20 @@ class Period(Protocol): @abc.abstractmethod def start(self) -> Any: """Abstract method.""" + @property @abc.abstractmethod def unit(self) -> Any: """Abstract method.""" + @abc.abstractmethod + def offset(self, offset: Any, unit: Any = None) -> Any: + """Abstract method.""" + + @abc.abstractmethod + def stop(self) -> Any: + """Abstract method.""" + class Population(Protocol): """Population protocol.""" From 09fab88a74aa9e8fe9255b00f2a2a57dbc415455 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 15:20:06 +0200 Subject: [PATCH 07/93] Add tests to periods.instant --- openfisca_core/periods/tests/__init__.py | 0 openfisca_core/periods/tests/test_helpers.py | 64 ++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 openfisca_core/periods/tests/__init__.py create mode 100644 openfisca_core/periods/tests/test_helpers.py 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/test_helpers.py b/openfisca_core/periods/tests/test_helpers.py new file mode 100644 index 0000000000..c20445b72d --- /dev/null +++ b/openfisca_core/periods/tests/test_helpers.py @@ -0,0 +1,64 @@ +import datetime + +import pytest + +from openfisca_core import periods +from openfisca_core.periods import DateUnit, Instant, Period + + +def test_instant(): + assert periods.instant((2022, 1, 1)) == Instant((2022, 1, 1)) + + +@pytest.mark.parametrize("arg, expected", [ + [None, None], + [datetime.date(1, 1, 1), Instant((1, 1, 1))], + [Instant((1, 1, 1)), Instant((1, 1, 1))], + [Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))], + [-1, Instant((-1, 1, 1))], + [0, Instant((0, 1, 1))], + [1, Instant((1, 1, 1))], + [999, Instant((999, 1, 1))], + [1000, Instant((1000, 1, 1))], + ["1000", Instant((1000, 1, 1))], + ["1000-01-01", Instant((1000, 1, 1))], + [(None,), Instant((None, 1, 1))], + [(None, None), Instant((None, None, 1))], + [(None, None, None), Instant((None, None, None))], + [(datetime.date(1, 1, 1),), Instant((datetime.date(1, 1, 1), 1, 1))], + [(Instant((1, 1, 1)),), Instant((Instant((1, 1, 1)), 1, 1))], + [(Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), Instant((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), 1, 1))], + [(-1,), Instant((-1, 1, 1))], + [(-1, -1), Instant((-1, -1, 1))], + [(-1, -1, -1), Instant((-1, -1, -1))], + [("-1",), Instant(("-1", 1, 1))], + [("-1", "-1"), Instant(("-1", "-1", 1))], + [("-1", "-1", "-1"), Instant(("-1", "-1", "-1"))], + [("1-1",), Instant(("1-1", 1, 1))], + [("1-1-1",), Instant(("1-1-1", 1, 1))], + ]) +def test_instant_with_a_valid_argument(arg, expected): + assert periods.instant(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + ["1000-0", ValueError], + ["1000-0-0", ValueError], + ["1000-1", ValueError], + ["1000-1-1", ValueError], + ["1", ValueError], + ["a", ValueError], + ["999", ValueError], + ["1:1000-01-01", ValueError], + ["a:1000-01-01", ValueError], + ["year:1000-01-01", ValueError], + ["year:1000-01-01:1", ValueError], + ["year:1000-01-01:3", ValueError], + ["1000-01-01:a", ValueError], + ["1000-01-01:1", ValueError], + [(), AssertionError], + [(None, None, None, None), AssertionError], + ]) +def test_instant_with_an_invalid_argument(arg, error): + with pytest.raises(error): + periods.instant(arg) \ No newline at end of file From 2ecbdb8d4905d2e12fea3b898c31e6dcf07cc576 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 16:17:00 +0200 Subject: [PATCH 08/93] Add tests to periods.instant_date --- .../periods/tests/helpers/__init__.py | 0 .../test_instant.py} | 6 +-- .../tests/helpers/test_instant_date.py | 42 +++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 openfisca_core/periods/tests/helpers/__init__.py rename openfisca_core/periods/tests/{test_helpers.py => helpers/test_instant.py} (94%) create mode 100644 openfisca_core/periods/tests/helpers/test_instant_date.py diff --git a/openfisca_core/periods/tests/helpers/__init__.py b/openfisca_core/periods/tests/helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/periods/tests/test_helpers.py b/openfisca_core/periods/tests/helpers/test_instant.py similarity index 94% rename from openfisca_core/periods/tests/test_helpers.py rename to openfisca_core/periods/tests/helpers/test_instant.py index c20445b72d..c96a467f54 100644 --- a/openfisca_core/periods/tests/test_helpers.py +++ b/openfisca_core/periods/tests/helpers/test_instant.py @@ -7,7 +7,7 @@ def test_instant(): - assert periods.instant((2022, 1, 1)) == Instant((2022, 1, 1)) + assert periods.instant((2022, 1, 1)) == Instant((2022, 1, 1)) @pytest.mark.parametrize("arg, expected", [ @@ -44,7 +44,7 @@ def test_instant_with_a_valid_argument(arg, expected): @pytest.mark.parametrize("arg, error", [ ["1000-0", ValueError], ["1000-0-0", ValueError], - ["1000-1", ValueError], + ["1000-1", ValueError], ["1000-1-1", ValueError], ["1", ValueError], ["a", ValueError], @@ -61,4 +61,4 @@ def test_instant_with_a_valid_argument(arg, expected): ]) def test_instant_with_an_invalid_argument(arg, error): with pytest.raises(error): - periods.instant(arg) \ No newline at end of file + periods.instant(arg) diff --git a/openfisca_core/periods/tests/helpers/test_instant_date.py b/openfisca_core/periods/tests/helpers/test_instant_date.py new file mode 100644 index 0000000000..bd1872b079 --- /dev/null +++ b/openfisca_core/periods/tests/helpers/test_instant_date.py @@ -0,0 +1,42 @@ +import datetime + +import pytest + +from openfisca_core import periods +from openfisca_core.periods import DateUnit, Instant, Period + + +def test_instant_date(): + assert periods.instant_date((2022, 1, 1)) == datetime.date(2022, 1, 1) + + +@pytest.mark.parametrize("arg, expected", [ + [None, None], + [Instant((1, 1, 1)), datetime.date(1, 1, 1)], + [Instant((4, 2, 29)), datetime.date(4, 2, 29)], + [(1, 1, 1), datetime.date(1, 1, 1)], + ]) +def test_instant_date_with_a_valid_argument(arg, expected): + assert periods.instant_date(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + [datetime.date(1, 1, 1), TypeError], + [Instant((-1, 1, 1)), ValueError], + [Instant((1, -1, 1)), ValueError], + [Instant((1, 13, -1)), ValueError], + [Instant((1, 1, -1)), ValueError], + [Instant((1, 1, 32)), ValueError], + [Instant((1, 2, 29)), ValueError], + [Instant(("1", 1, 1)), TypeError], + [Period((DateUnit.YEAR, Instant((1, 1, 1)), 1)), TypeError], + [1, TypeError], + ["1", TypeError], + [(), TypeError], + [(Instant((1, 1, 1)),), TypeError], + [(1,), TypeError], + [(1, 1), TypeError], + ]) +def test_instant_date_with_an_invalid_argument(arg, error): + with pytest.raises(error): + periods.instant_date(arg) From 0e51ac5b6a61657c90fbc6b25cf35d0b2dfb9523 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 16:37:19 +0200 Subject: [PATCH 09/93] Add tests to periods.key_period_size --- .../periods/tests/helpers/test_instant.py | 4 --- .../tests/helpers/test_instant_date.py | 4 --- .../tests/helpers/test_key_period_size.py | 32 +++++++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 openfisca_core/periods/tests/helpers/test_key_period_size.py diff --git a/openfisca_core/periods/tests/helpers/test_instant.py b/openfisca_core/periods/tests/helpers/test_instant.py index c96a467f54..d05e39ef5c 100644 --- a/openfisca_core/periods/tests/helpers/test_instant.py +++ b/openfisca_core/periods/tests/helpers/test_instant.py @@ -6,10 +6,6 @@ from openfisca_core.periods import DateUnit, Instant, Period -def test_instant(): - assert periods.instant((2022, 1, 1)) == Instant((2022, 1, 1)) - - @pytest.mark.parametrize("arg, expected", [ [None, None], [datetime.date(1, 1, 1), Instant((1, 1, 1))], diff --git a/openfisca_core/periods/tests/helpers/test_instant_date.py b/openfisca_core/periods/tests/helpers/test_instant_date.py index bd1872b079..474628a76c 100644 --- a/openfisca_core/periods/tests/helpers/test_instant_date.py +++ b/openfisca_core/periods/tests/helpers/test_instant_date.py @@ -6,10 +6,6 @@ from openfisca_core.periods import DateUnit, Instant, Period -def test_instant_date(): - assert periods.instant_date((2022, 1, 1)) == datetime.date(2022, 1, 1) - - @pytest.mark.parametrize("arg, expected", [ [None, None], [Instant((1, 1, 1)), datetime.date(1, 1, 1)], diff --git a/openfisca_core/periods/tests/helpers/test_key_period_size.py b/openfisca_core/periods/tests/helpers/test_key_period_size.py new file mode 100644 index 0000000000..52f372f1dd --- /dev/null +++ b/openfisca_core/periods/tests/helpers/test_key_period_size.py @@ -0,0 +1,32 @@ +import datetime + +import pytest + +from openfisca_core import periods +from openfisca_core.periods import DateUnit, Instant, Period + + +@pytest.mark.parametrize("arg, expected", [ + [Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), "100_365"], + [Period((DateUnit.MONTH, Instant((1, 1, 1)), 12)), "200_12"], + [Period((DateUnit.YEAR, Instant((1, 1, 1)), 2)), "300_2"], + [Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"], + [(DateUnit.DAY, None, 1), "100_1"], + [(DateUnit.MONTH, None, -1000), "200_-1000"], + ]) +def test_key_period_size_with_a_valid_argument(arg, expected): + assert periods.key_period_size(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + [None, TypeError], + [Instant((1, 1, 1)), KeyError], + [1, TypeError], + ["1", ValueError], + ["111", KeyError], + [(), ValueError], + [(1, 1, 1), KeyError], + ]) +def test_key_period_size_with_an_invalid_argument(arg, error): + with pytest.raises(error): + periods.key_period_size(arg) From a7e9851c13fec5fdd10e07d2d521a6cd679921e9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 16:43:01 +0200 Subject: [PATCH 10/93] Add doctest to periods.unit_weight --- .../periods/tests/helpers/test_instant.py | 6 +++--- .../periods/tests/helpers/test_instant_date.py | 4 ++-- .../tests/helpers/test_key_period_size.py | 16 +++++++--------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/openfisca_core/periods/tests/helpers/test_instant.py b/openfisca_core/periods/tests/helpers/test_instant.py index d05e39ef5c..d147fcdbf9 100644 --- a/openfisca_core/periods/tests/helpers/test_instant.py +++ b/openfisca_core/periods/tests/helpers/test_instant.py @@ -3,14 +3,14 @@ import pytest from openfisca_core import periods -from openfisca_core.periods import DateUnit, Instant, Period +from openfisca_core.periods import Instant, Period @pytest.mark.parametrize("arg, expected", [ [None, None], [datetime.date(1, 1, 1), Instant((1, 1, 1))], [Instant((1, 1, 1)), Instant((1, 1, 1))], - [Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))], + [Period((periods.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))], [-1, Instant((-1, 1, 1))], [0, Instant((0, 1, 1))], [1, Instant((1, 1, 1))], @@ -23,7 +23,7 @@ [(None, None, None), Instant((None, None, None))], [(datetime.date(1, 1, 1),), Instant((datetime.date(1, 1, 1), 1, 1))], [(Instant((1, 1, 1)),), Instant((Instant((1, 1, 1)), 1, 1))], - [(Period((DateUnit.DAY, Instant((1, 1, 1)), 365)),), Instant((Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), 1, 1))], + [(Period((periods.DAY, Instant((1, 1, 1)), 365)),), Instant((Period((periods.DAY, Instant((1, 1, 1)), 365)), 1, 1))], [(-1,), Instant((-1, 1, 1))], [(-1, -1), Instant((-1, -1, 1))], [(-1, -1, -1), Instant((-1, -1, -1))], diff --git a/openfisca_core/periods/tests/helpers/test_instant_date.py b/openfisca_core/periods/tests/helpers/test_instant_date.py index 474628a76c..22bd459d0f 100644 --- a/openfisca_core/periods/tests/helpers/test_instant_date.py +++ b/openfisca_core/periods/tests/helpers/test_instant_date.py @@ -3,7 +3,7 @@ import pytest from openfisca_core import periods -from openfisca_core.periods import DateUnit, Instant, Period +from openfisca_core.periods import Instant, Period @pytest.mark.parametrize("arg, expected", [ @@ -25,7 +25,7 @@ def test_instant_date_with_a_valid_argument(arg, expected): [Instant((1, 1, 32)), ValueError], [Instant((1, 2, 29)), ValueError], [Instant(("1", 1, 1)), TypeError], - [Period((DateUnit.YEAR, Instant((1, 1, 1)), 1)), TypeError], + [Period((periods.YEAR, Instant((1, 1, 1)), 1)), TypeError], [1, TypeError], ["1", TypeError], [(), TypeError], diff --git a/openfisca_core/periods/tests/helpers/test_key_period_size.py b/openfisca_core/periods/tests/helpers/test_key_period_size.py index 52f372f1dd..6f8acc17c9 100644 --- a/openfisca_core/periods/tests/helpers/test_key_period_size.py +++ b/openfisca_core/periods/tests/helpers/test_key_period_size.py @@ -1,18 +1,16 @@ -import datetime - import pytest from openfisca_core import periods -from openfisca_core.periods import DateUnit, Instant, Period +from openfisca_core.periods import Instant, Period @pytest.mark.parametrize("arg, expected", [ - [Period((DateUnit.DAY, Instant((1, 1, 1)), 365)), "100_365"], - [Period((DateUnit.MONTH, Instant((1, 1, 1)), 12)), "200_12"], - [Period((DateUnit.YEAR, Instant((1, 1, 1)), 2)), "300_2"], - [Period((DateUnit.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"], - [(DateUnit.DAY, None, 1), "100_1"], - [(DateUnit.MONTH, None, -1000), "200_-1000"], + [Period((periods.DAY, Instant((1, 1, 1)), 365)), "100_365"], + [Period((periods.MONTH, Instant((1, 1, 1)), 12)), "200_12"], + [Period((periods.YEAR, Instant((1, 1, 1)), 2)), "300_2"], + [Period((periods.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"], + [(periods.DAY, None, 1), "100_1"], + [(periods.MONTH, None, -1000), "200_-1000"], ]) def test_key_period_size_with_a_valid_argument(arg, expected): assert periods.key_period_size(arg) == expected From aff6d23f06640a87c08efba5aa93c6efbaa7f7ba Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 17:40:45 +0200 Subject: [PATCH 11/93] Extract parse simple period from builder --- openfisca_core/periods/period_.py | 8 ++++---- .../helpers/test__parse_simple_period.py | 19 +++++++++++++++++++ .../periods/tests/helpers/test_instant.py | 5 +++++ .../tests/helpers/test_instant_date.py | 8 +------- .../tests/helpers/test_key_period_size.py | 14 -------------- .../periods/tests/helpers/test_period.py | 0 6 files changed, 29 insertions(+), 25 deletions(-) create mode 100644 openfisca_core/periods/tests/helpers/test__parse_simple_period.py create mode 100644 openfisca_core/periods/tests/helpers/test_period.py diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 464ea51020..d3a9170f8d 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -43,7 +43,7 @@ class Period(tuple): >>> dict([period, instant]) Traceback (most recent call last): - ValueError: dictionary update sequence element #0 has length 3; 2 is required + ValueError: dictionary update sequence element #0 has length 3... >>> list(period) ['year', Instant((2021, 9, 1)), 3] @@ -205,16 +205,16 @@ def days(self) -> int: return (self.stop.date - self.start.date).days + 1 def get_subperiods(self, unit: str) -> Sequence[types.Period]: - """Return the list of all the periods of unit ``unit`` contained in self. + """Return the list of all the periods of unit ``unit``. Examples: >>> period = Period(("year", Instant((2021, 1, 1)), 1)) >>> period.get_subperiods("month") - [Period(('month', Instant((2021, 1, 1)), 1)), ...2021, 12, 1)), 1))] + [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] >>> period = Period(("year", Instant((2021, 1, 1)), 2)) >>> period.get_subperiods("year") - [Period(('year', Instant((2021, 1, 1)), 1)), ...((2022, 1, 1)), 1))] + [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] """ diff --git a/openfisca_core/periods/tests/helpers/test__parse_simple_period.py b/openfisca_core/periods/tests/helpers/test__parse_simple_period.py new file mode 100644 index 0000000000..081d795e85 --- /dev/null +++ b/openfisca_core/periods/tests/helpers/test__parse_simple_period.py @@ -0,0 +1,19 @@ +import pytest + +from openfisca_core import periods +from openfisca_core.periods import Instant, Period, helpers + + +@pytest.mark.parametrize("arg, expected", [ + ["1", None], + ["999", None], + ["1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["1000-1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["1000-1-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1000-01-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1000-01-99", None], + ]) +def test__parse_simple_period_with_a_valid_argument(arg, expected): + assert helpers._parse_simple_period(arg) == expected diff --git a/openfisca_core/periods/tests/helpers/test_instant.py b/openfisca_core/periods/tests/helpers/test_instant.py index d147fcdbf9..dd6002c73f 100644 --- a/openfisca_core/periods/tests/helpers/test_instant.py +++ b/openfisca_core/periods/tests/helpers/test_instant.py @@ -17,6 +17,7 @@ [999, Instant((999, 1, 1))], [1000, Instant((1000, 1, 1))], ["1000", Instant((1000, 1, 1))], + ["1000-01", Instant((1000, 1, 1))], ["1000-01-01", Instant((1000, 1, 1))], [(None,), Instant((None, 1, 1))], [(None, None), Instant((None, None, 1))], @@ -38,12 +39,16 @@ def test_instant_with_a_valid_argument(arg, expected): @pytest.mark.parametrize("arg, error", [ + [periods.YEAR, ValueError], + [periods.ETERNITY, ValueError], ["1000-0", ValueError], ["1000-0-0", ValueError], ["1000-1", ValueError], ["1000-1-1", ValueError], ["1", ValueError], ["a", ValueError], + ["year", ValueError], + ["eternity", ValueError], ["999", ValueError], ["1:1000-01-01", ValueError], ["a:1000-01-01", ValueError], diff --git a/openfisca_core/periods/tests/helpers/test_instant_date.py b/openfisca_core/periods/tests/helpers/test_instant_date.py index 22bd459d0f..722728a6e4 100644 --- a/openfisca_core/periods/tests/helpers/test_instant_date.py +++ b/openfisca_core/periods/tests/helpers/test_instant_date.py @@ -3,7 +3,7 @@ import pytest from openfisca_core import periods -from openfisca_core.periods import Instant, Period +from openfisca_core.periods import Instant @pytest.mark.parametrize("arg, expected", [ @@ -17,7 +17,6 @@ def test_instant_date_with_a_valid_argument(arg, expected): @pytest.mark.parametrize("arg, error", [ - [datetime.date(1, 1, 1), TypeError], [Instant((-1, 1, 1)), ValueError], [Instant((1, -1, 1)), ValueError], [Instant((1, 13, -1)), ValueError], @@ -25,11 +24,6 @@ def test_instant_date_with_a_valid_argument(arg, expected): [Instant((1, 1, 32)), ValueError], [Instant((1, 2, 29)), ValueError], [Instant(("1", 1, 1)), TypeError], - [Period((periods.YEAR, Instant((1, 1, 1)), 1)), TypeError], - [1, TypeError], - ["1", TypeError], - [(), TypeError], - [(Instant((1, 1, 1)),), TypeError], [(1,), TypeError], [(1, 1), TypeError], ]) diff --git a/openfisca_core/periods/tests/helpers/test_key_period_size.py b/openfisca_core/periods/tests/helpers/test_key_period_size.py index 6f8acc17c9..1094d4e42e 100644 --- a/openfisca_core/periods/tests/helpers/test_key_period_size.py +++ b/openfisca_core/periods/tests/helpers/test_key_period_size.py @@ -14,17 +14,3 @@ ]) def test_key_period_size_with_a_valid_argument(arg, expected): assert periods.key_period_size(arg) == expected - - -@pytest.mark.parametrize("arg, error", [ - [None, TypeError], - [Instant((1, 1, 1)), KeyError], - [1, TypeError], - ["1", ValueError], - ["111", KeyError], - [(), ValueError], - [(1, 1, 1), KeyError], - ]) -def test_key_period_size_with_an_invalid_argument(arg, error): - with pytest.raises(error): - periods.key_period_size(arg) diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py new file mode 100644 index 0000000000..e69de29bb2 From 039468de0c648e20149865e3278d3a2436a3e54c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 18:16:21 +0200 Subject: [PATCH 12/93] Add tests to periods.period --- .../periods/tests/helpers/test_period.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py index e69de29bb2..4df00a9a18 100644 --- a/openfisca_core/periods/tests/helpers/test_period.py +++ b/openfisca_core/periods/tests/helpers/test_period.py @@ -0,0 +1,69 @@ +import datetime + +import pytest + +from openfisca_core import periods +from openfisca_core.periods import Instant, Period + + +@pytest.mark.parametrize("arg, expected", [ + ["eternity", Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], + ["ETERNITY", Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], + [periods.ETERNITY, Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], + [Instant((1, 1, 1)), Period((periods.DAY, Instant((1, 1, 1)), 1))], + [Period((periods.DAY, Instant((1, 1, 1)), 365)), Period((periods.DAY, Instant((1, 1, 1)), 365))], + [-1, Period((periods.YEAR, Instant((-1, 1, 1)), 1))], + [0, Period((periods.YEAR, Instant((0, 1, 1)), 1))], + [1, Period((periods.YEAR, Instant((1, 1, 1)), 1))], + [999, Period((periods.YEAR, Instant((999, 1, 1)), 1))], + [1000, Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["1000-1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["1000-1-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], + ]) +def test_instant_with_a_valid_argument(arg, expected): + assert periods.period(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + [None, ValueError], + [periods.YEAR, ValueError], + [datetime.date(1, 1, 1), ValueError], + ["1000-0", ValueError], + ["1000-0-0", ValueError], + ["1", ValueError], + ["a", ValueError], + ["year", ValueError], + ["999", ValueError], + ["1:1000-01-01", ValueError], + ["a:1000-01-01", ValueError], + ["1000-01-01:a", ValueError], + ["1000-01-01:1", ValueError], + [(), ValueError], + [(None,), ValueError], + [(None, None), ValueError], + [(None, None, None), ValueError], + [(None, None, None, None), ValueError], + [(datetime.date(1, 1, 1),), ValueError], + [(Instant((1, 1, 1)),), ValueError], + [(Period((periods.DAY, Instant((1, 1, 1)), 365)),), ValueError], + [(1,), ValueError], + [(1, 1), ValueError], + [(1, 1, 1), ValueError], + [(-1,), ValueError], + [(-1, -1), ValueError], + [(-1, -1, -1), ValueError], + [("-1",), ValueError], + [("-1", "-1"), ValueError], + [("-1", "-1", "-1"), ValueError], + [("1-1",), ValueError], + [("1-1-1",), ValueError], + ]) +def test_instant_with_an_invalid_argument(arg, error): + with pytest.raises(error): + periods.period(arg) From 85717fadcafc1a3a3ff8727a2a2633db6aabaeae Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 18:44:02 +0200 Subject: [PATCH 13/93] Refactor period's str year tests --- .../periods/tests/period/__init__.py | 0 .../periods/tests/period/test_str.py | 25 +++++++++++++++++++ tests/core/test_periods.py | 20 --------------- 3 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 openfisca_core/periods/tests/period/__init__.py create mode 100644 openfisca_core/periods/tests/period/test_str.py diff --git a/openfisca_core/periods/tests/period/__init__.py b/openfisca_core/periods/tests/period/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/periods/tests/period/test_str.py b/openfisca_core/periods/tests/period/test_str.py new file mode 100644 index 0000000000..45a2e588cb --- /dev/null +++ b/openfisca_core/periods/tests/period/test_str.py @@ -0,0 +1,25 @@ +import pytest + +from openfisca_core.periods import DateUnit, Instant, Period + + +@pytest.fixture +def first_jan(): + return Instant((2022, 1, 1)) + + +@pytest.fixture +def first_march(): + return Instant((2022, 3, 1)) + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [DateUnit.YEAR, Instant((2022, 1, 1)), 1, "2022"], + [DateUnit.MONTH, Instant((2022, 1, 1)), 12, "2022"], + [DateUnit.YEAR, Instant((2022, 3, 1)), 1, "year:2022-03"], + [DateUnit.MONTH, Instant((2022, 3, 1)), 12, "year:2022-03"], + [DateUnit.YEAR, Instant((2022, 1, 1)), 3, "year:2022:3"], + [DateUnit.YEAR, Instant((2022, 1, 3)), 3, "year:2022:3"], + ]) +def test_str_with_years(date_unit, instant, size, expected): + assert str(Period((date_unit, instant, size))) == expected \ No newline at end of file diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 2c125d527c..9e23b0d782 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -14,26 +14,6 @@ ''' -# 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(): From 3fc64b6464ddaab681c69f6ff8486c50f4e37773 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 18:46:23 +0200 Subject: [PATCH 14/93] Refactor period's str month tests --- .../periods/tests/period/test_str.py | 19 +++++++++---------- tests/core/test_periods.py | 7 ------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/openfisca_core/periods/tests/period/test_str.py b/openfisca_core/periods/tests/period/test_str.py index 45a2e588cb..f96102547b 100644 --- a/openfisca_core/periods/tests/period/test_str.py +++ b/openfisca_core/periods/tests/period/test_str.py @@ -3,16 +3,6 @@ from openfisca_core.periods import DateUnit, Instant, Period -@pytest.fixture -def first_jan(): - return Instant((2022, 1, 1)) - - -@pytest.fixture -def first_march(): - return Instant((2022, 3, 1)) - - @pytest.mark.parametrize("date_unit, instant, size, expected", [ [DateUnit.YEAR, Instant((2022, 1, 1)), 1, "2022"], [DateUnit.MONTH, Instant((2022, 1, 1)), 12, "2022"], @@ -22,4 +12,13 @@ def first_march(): [DateUnit.YEAR, Instant((2022, 1, 3)), 3, "year:2022:3"], ]) def test_str_with_years(date_unit, instant, size, expected): + assert str(Period((date_unit, instant, size))) == expected + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [DateUnit.MONTH, Instant((2022, 1, 1)), 1, "2022-01"], + [DateUnit.MONTH, Instant((2022, 1, 1)), 3, "month:2022-01:3"], + [DateUnit.MONTH, Instant((2022, 3, 1)), 3, "month:2022-03:3"], + ]) +def test_str_with_months(date_unit, instant, size, expected): assert str(Period((date_unit, instant, size))) == expected \ No newline at end of file diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 9e23b0d782..4ab5a3f91f 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -16,13 +16,6 @@ # 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 From 7503d2719bd650e5ce2e96b241bb69dc241baa24 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 18:48:23 +0200 Subject: [PATCH 15/93] Refactor period's str day tests --- .../periods/tests/period/test_str.py | 9 ++++++++ tests/core/test_periods.py | 21 ------------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/openfisca_core/periods/tests/period/test_str.py b/openfisca_core/periods/tests/period/test_str.py index f96102547b..47ea3c390d 100644 --- a/openfisca_core/periods/tests/period/test_str.py +++ b/openfisca_core/periods/tests/period/test_str.py @@ -21,4 +21,13 @@ def test_str_with_years(date_unit, instant, size, expected): [DateUnit.MONTH, Instant((2022, 3, 1)), 3, "month:2022-03:3"], ]) def test_str_with_months(date_unit, instant, size, expected): + assert str(Period((date_unit, instant, size))) == expected + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [DateUnit.DAY, Instant((2022, 1, 1)), 1, "2022-01-01"], + [DateUnit.DAY, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], + [DateUnit.DAY, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], + ]) +def test_str_with_days(date_unit, instant, size, expected): assert str(Period((date_unit, instant, size))) == expected \ No newline at end of file diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 4ab5a3f91f..92c8b49022 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -9,31 +9,10 @@ first_march = Instant((2014, 3, 1)) -''' -Test Period -> String -''' - - -# Months - - - -# 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(): From fed7c4a8d1008a1deb6416b4f185cc90224eb2db Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 18:54:46 +0200 Subject: [PATCH 16/93] Remove redundant examples --- openfisca_core/periods/period_.py | 4 +++- openfisca_core/periods/tests/period/test_str.py | 2 +- tests/core/test_periods.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index d3a9170f8d..5d5b0f58b6 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -165,8 +165,10 @@ def __str__(self) -> str: """ unit, start_instant, size = self + if unit == config.ETERNITY: - return 'ETERNITY' + return "ETERNITY" + year, month, day = start_instant # 1 year long period diff --git a/openfisca_core/periods/tests/period/test_str.py b/openfisca_core/periods/tests/period/test_str.py index 47ea3c390d..874a8405ab 100644 --- a/openfisca_core/periods/tests/period/test_str.py +++ b/openfisca_core/periods/tests/period/test_str.py @@ -30,4 +30,4 @@ def test_str_with_months(date_unit, instant, size, expected): [DateUnit.DAY, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], ]) def test_str_with_days(date_unit, instant, size, expected): - assert str(Period((date_unit, instant, size))) == expected \ No newline at end of file + assert str(Period((date_unit, instant, size))) == expected diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 92c8b49022..58fffd7919 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -15,6 +15,7 @@ # Years + def test_parsing_year(): assert period('2014') == Period((YEAR, first_jan, 1)) From bcd1edc223bca789b5e2b15441880012b8d0a875 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 19:35:31 +0200 Subject: [PATCH 17/93] Redistribute tests --- openfisca_core/periods/period_.py | 1 - .../periods/tests/helpers/test_instant.py | 2 + .../periods/tests/helpers/test_period.py | 35 ++++- .../periods/tests/period/test_size_in_days.py | 20 +++ .../periods/tests/period/test_str.py | 27 ++-- tests/core/test_periods.py | 132 +----------------- 6 files changed, 68 insertions(+), 149 deletions(-) create mode 100644 openfisca_core/periods/tests/period/test_size_in_days.py diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 5d5b0f58b6..20c301a8b7 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -203,7 +203,6 @@ def date(self) -> datetime.date: @property def days(self) -> int: """Count the number of days in period.""" - return (self.stop.date - self.start.date).days + 1 def get_subperiods(self, unit: str) -> Sequence[types.Period]: diff --git a/openfisca_core/periods/tests/helpers/test_instant.py b/openfisca_core/periods/tests/helpers/test_instant.py index dd6002c73f..c58c5897f2 100644 --- a/openfisca_core/periods/tests/helpers/test_instant.py +++ b/openfisca_core/periods/tests/helpers/test_instant.py @@ -58,6 +58,8 @@ def test_instant_with_a_valid_argument(arg, expected): ["1000-01-01:a", ValueError], ["1000-01-01:1", ValueError], [(), AssertionError], + [{}, AssertionError], + ["", ValueError], [(None, None, None, None), AssertionError], ]) def test_instant_with_an_invalid_argument(arg, error): diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py index 4df00a9a18..50cc59eae8 100644 --- a/openfisca_core/periods/tests/helpers/test_period.py +++ b/openfisca_core/periods/tests/helpers/test_period.py @@ -22,9 +22,24 @@ ["1000-1-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], ["1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1004-02-29", Period((periods.DAY, Instant((1004, 2, 29)), 1))], + ["year:1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], ["year:1000-01-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000-01:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], ["year:1000-01-01:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], + ["year:1000:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], + ["year:1000-01:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], + ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["month:1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["month:1000-01-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["month:1000-01:1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["month:1000-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["day:1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["day:1000-01-01:3", Period((periods.DAY, Instant((1000, 1, 1)), 3))], ]) def test_instant_with_a_valid_argument(arg, expected): assert periods.period(arg) == expected @@ -35,16 +50,28 @@ def test_instant_with_a_valid_argument(arg, expected): [periods.YEAR, ValueError], [datetime.date(1, 1, 1), ValueError], ["1000-0", ValueError], + ["1000-13", ValueError], ["1000-0-0", ValueError], + ["1000-1-0", ValueError], + ["1000-2-31", ValueError], ["1", ValueError], ["a", ValueError], ["year", ValueError], ["999", ValueError], - ["1:1000-01-01", ValueError], - ["a:1000-01-01", ValueError], - ["1000-01-01:a", ValueError], + ["1:1000", ValueError], + ["a:1000", ValueError], + ["month:1000", ValueError], + ["day:1000-01", ValueError], + ["1000:a", ValueError], + ["1000:1", ValueError], + ["1000-01:1", ValueError], ["1000-01-01:1", ValueError], + ["month:1000:1", ValueError], + ["day:1000:1", ValueError], + ["day:1000-01:1", ValueError], [(), ValueError], + [{}, ValueError], + ["", ValueError], [(None,), ValueError], [(None, None), ValueError], [(None, None, None), ValueError], diff --git a/openfisca_core/periods/tests/period/test_size_in_days.py b/openfisca_core/periods/tests/period/test_size_in_days.py new file mode 100644 index 0000000000..c68d5d82b0 --- /dev/null +++ b/openfisca_core/periods/tests/period/test_size_in_days.py @@ -0,0 +1,20 @@ +import pytest + +from openfisca_core import periods +from openfisca_core.periods import Instant, Period + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.DAY, Instant((2022, 12, 31)), 1, 1], + [periods.DAY, Instant((2022, 12, 31)), 3, 3], + [periods.MONTH, Instant((2022, 12, 1)), 1, 31], + [periods.MONTH, Instant((2012, 2, 3)), 1, 29], + [periods.MONTH, Instant((2022, 1, 3)), 3, 31 + 28 + 31], + [periods.MONTH, Instant((2012, 1, 3)), 3, 31 + 29 + 31], + [periods.YEAR, Instant((2022, 12, 1)), 1, 365], + [periods.YEAR, Instant((2012, 1, 1)), 1, 366], + [periods.YEAR, Instant((2022, 1, 1)), 2, 730], + ]) +def test_day_size_in_days(date_unit, instant, size, expected): + period = Period((date_unit, instant, size)) + assert period.size_in_days == expected diff --git a/openfisca_core/periods/tests/period/test_str.py b/openfisca_core/periods/tests/period/test_str.py index 874a8405ab..4e0ba81446 100644 --- a/openfisca_core/periods/tests/period/test_str.py +++ b/openfisca_core/periods/tests/period/test_str.py @@ -1,33 +1,34 @@ import pytest -from openfisca_core.periods import DateUnit, Instant, Period +from openfisca_core import periods +from openfisca_core.periods import Instant, Period @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [DateUnit.YEAR, Instant((2022, 1, 1)), 1, "2022"], - [DateUnit.MONTH, Instant((2022, 1, 1)), 12, "2022"], - [DateUnit.YEAR, Instant((2022, 3, 1)), 1, "year:2022-03"], - [DateUnit.MONTH, Instant((2022, 3, 1)), 12, "year:2022-03"], - [DateUnit.YEAR, Instant((2022, 1, 1)), 3, "year:2022:3"], - [DateUnit.YEAR, Instant((2022, 1, 3)), 3, "year:2022:3"], + [periods.YEAR, Instant((2022, 1, 1)), 1, "2022"], + [periods.MONTH, Instant((2022, 1, 1)), 12, "2022"], + [periods.YEAR, Instant((2022, 3, 1)), 1, "year:2022-03"], + [periods.MONTH, Instant((2022, 3, 1)), 12, "year:2022-03"], + [periods.YEAR, Instant((2022, 1, 1)), 3, "year:2022:3"], + [periods.YEAR, Instant((2022, 1, 3)), 3, "year:2022:3"], ]) def test_str_with_years(date_unit, instant, size, expected): assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [DateUnit.MONTH, Instant((2022, 1, 1)), 1, "2022-01"], - [DateUnit.MONTH, Instant((2022, 1, 1)), 3, "month:2022-01:3"], - [DateUnit.MONTH, Instant((2022, 3, 1)), 3, "month:2022-03:3"], + [periods.MONTH, Instant((2022, 1, 1)), 1, "2022-01"], + [periods.MONTH, Instant((2022, 1, 1)), 3, "month:2022-01:3"], + [periods.MONTH, Instant((2022, 3, 1)), 3, "month:2022-03:3"], ]) def test_str_with_months(date_unit, instant, size, expected): assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [DateUnit.DAY, Instant((2022, 1, 1)), 1, "2022-01-01"], - [DateUnit.DAY, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], - [DateUnit.DAY, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], + [periods.DAY, Instant((2022, 1, 1)), 1, "2022-01-01"], + [periods.DAY, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], + [periods.DAY, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], ]) def test_str_with_days(date_unit, instant, size, expected): assert str(Period((date_unit, instant, size))) == expected diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 58fffd7919..0816a0ce2e 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -3,137 +3,7 @@ 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 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('') +from openfisca_core.periods import YEAR, MONTH, DAY, period @pytest.mark.parametrize("test", [ From 309d58ef2a06e63670cdf27f7e74db1b7c67e463 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 30 Jul 2022 20:26:35 +0200 Subject: [PATCH 18/93] Consolidate tests --- .../periods/tests/helpers/__init__.py | 0 .../helpers/test__parse_simple_period.py | 19 -- .../periods/tests/helpers/test_instant.py | 67 ------ .../tests/helpers/test_instant_date.py | 32 --- .../tests/helpers/test_key_period_size.py | 16 -- .../periods/tests/helpers/test_period.py | 96 -------- .../periods/tests/period/__init__.py | 0 .../periods/tests/period/test_size_in_days.py | 20 -- .../periods/tests/period/test_str.py | 34 --- openfisca_core/periods/tests/test_helpers.py | 210 ++++++++++++++++++ openfisca_core/periods/tests/test_period.py | 80 +++++++ tests/core/test_periods.py | 26 --- 12 files changed, 290 insertions(+), 310 deletions(-) delete mode 100644 openfisca_core/periods/tests/helpers/__init__.py delete mode 100644 openfisca_core/periods/tests/helpers/test__parse_simple_period.py delete mode 100644 openfisca_core/periods/tests/helpers/test_instant.py delete mode 100644 openfisca_core/periods/tests/helpers/test_instant_date.py delete mode 100644 openfisca_core/periods/tests/helpers/test_key_period_size.py delete mode 100644 openfisca_core/periods/tests/helpers/test_period.py delete mode 100644 openfisca_core/periods/tests/period/__init__.py delete mode 100644 openfisca_core/periods/tests/period/test_size_in_days.py delete mode 100644 openfisca_core/periods/tests/period/test_str.py create mode 100644 openfisca_core/periods/tests/test_helpers.py create mode 100644 openfisca_core/periods/tests/test_period.py delete mode 100644 tests/core/test_periods.py diff --git a/openfisca_core/periods/tests/helpers/__init__.py b/openfisca_core/periods/tests/helpers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openfisca_core/periods/tests/helpers/test__parse_simple_period.py b/openfisca_core/periods/tests/helpers/test__parse_simple_period.py deleted file mode 100644 index 081d795e85..0000000000 --- a/openfisca_core/periods/tests/helpers/test__parse_simple_period.py +++ /dev/null @@ -1,19 +0,0 @@ -import pytest - -from openfisca_core import periods -from openfisca_core.periods import Instant, Period, helpers - - -@pytest.mark.parametrize("arg, expected", [ - ["1", None], - ["999", None], - ["1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["1000-1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["1000-1-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["1000-01-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["1000-01-99", None], - ]) -def test__parse_simple_period_with_a_valid_argument(arg, expected): - assert helpers._parse_simple_period(arg) == expected diff --git a/openfisca_core/periods/tests/helpers/test_instant.py b/openfisca_core/periods/tests/helpers/test_instant.py deleted file mode 100644 index c58c5897f2..0000000000 --- a/openfisca_core/periods/tests/helpers/test_instant.py +++ /dev/null @@ -1,67 +0,0 @@ -import datetime - -import pytest - -from openfisca_core import periods -from openfisca_core.periods import Instant, Period - - -@pytest.mark.parametrize("arg, expected", [ - [None, None], - [datetime.date(1, 1, 1), Instant((1, 1, 1))], - [Instant((1, 1, 1)), Instant((1, 1, 1))], - [Period((periods.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))], - [-1, Instant((-1, 1, 1))], - [0, Instant((0, 1, 1))], - [1, Instant((1, 1, 1))], - [999, Instant((999, 1, 1))], - [1000, Instant((1000, 1, 1))], - ["1000", Instant((1000, 1, 1))], - ["1000-01", Instant((1000, 1, 1))], - ["1000-01-01", Instant((1000, 1, 1))], - [(None,), Instant((None, 1, 1))], - [(None, None), Instant((None, None, 1))], - [(None, None, None), Instant((None, None, None))], - [(datetime.date(1, 1, 1),), Instant((datetime.date(1, 1, 1), 1, 1))], - [(Instant((1, 1, 1)),), Instant((Instant((1, 1, 1)), 1, 1))], - [(Period((periods.DAY, Instant((1, 1, 1)), 365)),), Instant((Period((periods.DAY, Instant((1, 1, 1)), 365)), 1, 1))], - [(-1,), Instant((-1, 1, 1))], - [(-1, -1), Instant((-1, -1, 1))], - [(-1, -1, -1), Instant((-1, -1, -1))], - [("-1",), Instant(("-1", 1, 1))], - [("-1", "-1"), Instant(("-1", "-1", 1))], - [("-1", "-1", "-1"), Instant(("-1", "-1", "-1"))], - [("1-1",), Instant(("1-1", 1, 1))], - [("1-1-1",), Instant(("1-1-1", 1, 1))], - ]) -def test_instant_with_a_valid_argument(arg, expected): - assert periods.instant(arg) == expected - - -@pytest.mark.parametrize("arg, error", [ - [periods.YEAR, ValueError], - [periods.ETERNITY, ValueError], - ["1000-0", ValueError], - ["1000-0-0", ValueError], - ["1000-1", ValueError], - ["1000-1-1", ValueError], - ["1", ValueError], - ["a", ValueError], - ["year", ValueError], - ["eternity", ValueError], - ["999", ValueError], - ["1:1000-01-01", ValueError], - ["a:1000-01-01", ValueError], - ["year:1000-01-01", ValueError], - ["year:1000-01-01:1", ValueError], - ["year:1000-01-01:3", ValueError], - ["1000-01-01:a", ValueError], - ["1000-01-01:1", ValueError], - [(), AssertionError], - [{}, AssertionError], - ["", ValueError], - [(None, None, None, None), AssertionError], - ]) -def test_instant_with_an_invalid_argument(arg, error): - with pytest.raises(error): - periods.instant(arg) diff --git a/openfisca_core/periods/tests/helpers/test_instant_date.py b/openfisca_core/periods/tests/helpers/test_instant_date.py deleted file mode 100644 index 722728a6e4..0000000000 --- a/openfisca_core/periods/tests/helpers/test_instant_date.py +++ /dev/null @@ -1,32 +0,0 @@ -import datetime - -import pytest - -from openfisca_core import periods -from openfisca_core.periods import Instant - - -@pytest.mark.parametrize("arg, expected", [ - [None, None], - [Instant((1, 1, 1)), datetime.date(1, 1, 1)], - [Instant((4, 2, 29)), datetime.date(4, 2, 29)], - [(1, 1, 1), datetime.date(1, 1, 1)], - ]) -def test_instant_date_with_a_valid_argument(arg, expected): - assert periods.instant_date(arg) == expected - - -@pytest.mark.parametrize("arg, error", [ - [Instant((-1, 1, 1)), ValueError], - [Instant((1, -1, 1)), ValueError], - [Instant((1, 13, -1)), ValueError], - [Instant((1, 1, -1)), ValueError], - [Instant((1, 1, 32)), ValueError], - [Instant((1, 2, 29)), ValueError], - [Instant(("1", 1, 1)), TypeError], - [(1,), TypeError], - [(1, 1), TypeError], - ]) -def test_instant_date_with_an_invalid_argument(arg, error): - with pytest.raises(error): - periods.instant_date(arg) diff --git a/openfisca_core/periods/tests/helpers/test_key_period_size.py b/openfisca_core/periods/tests/helpers/test_key_period_size.py deleted file mode 100644 index 1094d4e42e..0000000000 --- a/openfisca_core/periods/tests/helpers/test_key_period_size.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest - -from openfisca_core import periods -from openfisca_core.periods import Instant, Period - - -@pytest.mark.parametrize("arg, expected", [ - [Period((periods.DAY, Instant((1, 1, 1)), 365)), "100_365"], - [Period((periods.MONTH, Instant((1, 1, 1)), 12)), "200_12"], - [Period((periods.YEAR, Instant((1, 1, 1)), 2)), "300_2"], - [Period((periods.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"], - [(periods.DAY, None, 1), "100_1"], - [(periods.MONTH, None, -1000), "200_-1000"], - ]) -def test_key_period_size_with_a_valid_argument(arg, expected): - assert periods.key_period_size(arg) == expected diff --git a/openfisca_core/periods/tests/helpers/test_period.py b/openfisca_core/periods/tests/helpers/test_period.py deleted file mode 100644 index 50cc59eae8..0000000000 --- a/openfisca_core/periods/tests/helpers/test_period.py +++ /dev/null @@ -1,96 +0,0 @@ -import datetime - -import pytest - -from openfisca_core import periods -from openfisca_core.periods import Instant, Period - - -@pytest.mark.parametrize("arg, expected", [ - ["eternity", Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], - ["ETERNITY", Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], - [periods.ETERNITY, Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], - [Instant((1, 1, 1)), Period((periods.DAY, Instant((1, 1, 1)), 1))], - [Period((periods.DAY, Instant((1, 1, 1)), 365)), Period((periods.DAY, Instant((1, 1, 1)), 365))], - [-1, Period((periods.YEAR, Instant((-1, 1, 1)), 1))], - [0, Period((periods.YEAR, Instant((0, 1, 1)), 1))], - [1, Period((periods.YEAR, Instant((1, 1, 1)), 1))], - [999, Period((periods.YEAR, Instant((999, 1, 1)), 1))], - [1000, Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["1000-1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["1000-1-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["1004-02-29", Period((periods.DAY, Instant((1004, 2, 29)), 1))], - ["year:1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], - ["year:1000-01:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], - ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], - ["month:1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["month:1000-01-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["month:1000-01:1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["month:1000-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], - ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], - ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], - ["day:1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["day:1000-01-01:3", Period((periods.DAY, Instant((1000, 1, 1)), 3))], - ]) -def test_instant_with_a_valid_argument(arg, expected): - assert periods.period(arg) == expected - - -@pytest.mark.parametrize("arg, error", [ - [None, ValueError], - [periods.YEAR, ValueError], - [datetime.date(1, 1, 1), ValueError], - ["1000-0", ValueError], - ["1000-13", ValueError], - ["1000-0-0", ValueError], - ["1000-1-0", ValueError], - ["1000-2-31", ValueError], - ["1", ValueError], - ["a", ValueError], - ["year", ValueError], - ["999", ValueError], - ["1:1000", ValueError], - ["a:1000", ValueError], - ["month:1000", ValueError], - ["day:1000-01", ValueError], - ["1000:a", ValueError], - ["1000:1", ValueError], - ["1000-01:1", ValueError], - ["1000-01-01:1", ValueError], - ["month:1000:1", ValueError], - ["day:1000:1", ValueError], - ["day:1000-01:1", ValueError], - [(), ValueError], - [{}, ValueError], - ["", ValueError], - [(None,), ValueError], - [(None, None), ValueError], - [(None, None, None), ValueError], - [(None, None, None, None), ValueError], - [(datetime.date(1, 1, 1),), ValueError], - [(Instant((1, 1, 1)),), ValueError], - [(Period((periods.DAY, Instant((1, 1, 1)), 365)),), ValueError], - [(1,), ValueError], - [(1, 1), ValueError], - [(1, 1, 1), ValueError], - [(-1,), ValueError], - [(-1, -1), ValueError], - [(-1, -1, -1), ValueError], - [("-1",), ValueError], - [("-1", "-1"), ValueError], - [("-1", "-1", "-1"), ValueError], - [("1-1",), ValueError], - [("1-1-1",), ValueError], - ]) -def test_instant_with_an_invalid_argument(arg, error): - with pytest.raises(error): - periods.period(arg) diff --git a/openfisca_core/periods/tests/period/__init__.py b/openfisca_core/periods/tests/period/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openfisca_core/periods/tests/period/test_size_in_days.py b/openfisca_core/periods/tests/period/test_size_in_days.py deleted file mode 100644 index c68d5d82b0..0000000000 --- a/openfisca_core/periods/tests/period/test_size_in_days.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest - -from openfisca_core import periods -from openfisca_core.periods import Instant, Period - - -@pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.DAY, Instant((2022, 12, 31)), 1, 1], - [periods.DAY, Instant((2022, 12, 31)), 3, 3], - [periods.MONTH, Instant((2022, 12, 1)), 1, 31], - [periods.MONTH, Instant((2012, 2, 3)), 1, 29], - [periods.MONTH, Instant((2022, 1, 3)), 3, 31 + 28 + 31], - [periods.MONTH, Instant((2012, 1, 3)), 3, 31 + 29 + 31], - [periods.YEAR, Instant((2022, 12, 1)), 1, 365], - [periods.YEAR, Instant((2012, 1, 1)), 1, 366], - [periods.YEAR, Instant((2022, 1, 1)), 2, 730], - ]) -def test_day_size_in_days(date_unit, instant, size, expected): - period = Period((date_unit, instant, size)) - assert period.size_in_days == expected diff --git a/openfisca_core/periods/tests/period/test_str.py b/openfisca_core/periods/tests/period/test_str.py deleted file mode 100644 index 4e0ba81446..0000000000 --- a/openfisca_core/periods/tests/period/test_str.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from openfisca_core import periods -from openfisca_core.periods import Instant, Period - - -@pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.YEAR, Instant((2022, 1, 1)), 1, "2022"], - [periods.MONTH, Instant((2022, 1, 1)), 12, "2022"], - [periods.YEAR, Instant((2022, 3, 1)), 1, "year:2022-03"], - [periods.MONTH, Instant((2022, 3, 1)), 12, "year:2022-03"], - [periods.YEAR, Instant((2022, 1, 1)), 3, "year:2022:3"], - [periods.YEAR, Instant((2022, 1, 3)), 3, "year:2022:3"], - ]) -def test_str_with_years(date_unit, instant, size, expected): - assert str(Period((date_unit, instant, size))) == expected - - -@pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, Instant((2022, 1, 1)), 1, "2022-01"], - [periods.MONTH, Instant((2022, 1, 1)), 3, "month:2022-01:3"], - [periods.MONTH, Instant((2022, 3, 1)), 3, "month:2022-03:3"], - ]) -def test_str_with_months(date_unit, instant, size, expected): - assert str(Period((date_unit, instant, size))) == expected - - -@pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.DAY, Instant((2022, 1, 1)), 1, "2022-01-01"], - [periods.DAY, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], - [periods.DAY, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], - ]) -def test_str_with_days(date_unit, instant, size, expected): - assert str(Period((date_unit, instant, size))) == expected diff --git a/openfisca_core/periods/tests/test_helpers.py b/openfisca_core/periods/tests/test_helpers.py new file mode 100644 index 0000000000..6dd09788b9 --- /dev/null +++ b/openfisca_core/periods/tests/test_helpers.py @@ -0,0 +1,210 @@ +import datetime + +import pytest + +from openfisca_core import periods +from openfisca_core.periods import Instant, Period, helpers + + +@pytest.mark.parametrize("arg, expected", [ + [None, None], + [datetime.date(1, 1, 1), Instant((1, 1, 1))], + [Instant((1, 1, 1)), Instant((1, 1, 1))], + [Period((periods.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))], + [-1, Instant((-1, 1, 1))], + [0, Instant((0, 1, 1))], + [1, Instant((1, 1, 1))], + [999, Instant((999, 1, 1))], + [1000, Instant((1000, 1, 1))], + ["1000", Instant((1000, 1, 1))], + ["1000-01", Instant((1000, 1, 1))], + ["1000-01-01", Instant((1000, 1, 1))], + [(None,), Instant((None, 1, 1))], + [(None, None), Instant((None, None, 1))], + [(None, None, None), Instant((None, None, None))], + [(datetime.date(1, 1, 1),), Instant((datetime.date(1, 1, 1), 1, 1))], + [(Instant((1, 1, 1)),), Instant((Instant((1, 1, 1)), 1, 1))], + [(Period((periods.DAY, Instant((1, 1, 1)), 365)),), Instant((Period((periods.DAY, Instant((1, 1, 1)), 365)), 1, 1))], + [(-1,), Instant((-1, 1, 1))], + [(-1, -1), Instant((-1, -1, 1))], + [(-1, -1, -1), Instant((-1, -1, -1))], + [("-1",), Instant(("-1", 1, 1))], + [("-1", "-1"), Instant(("-1", "-1", 1))], + [("-1", "-1", "-1"), Instant(("-1", "-1", "-1"))], + [("1-1",), Instant(("1-1", 1, 1))], + [("1-1-1",), Instant(("1-1-1", 1, 1))], + ]) +def test_instant_with_a_valid_argument(arg, expected): + assert periods.instant(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + [periods.YEAR, ValueError], + [periods.ETERNITY, ValueError], + ["1000-0", ValueError], + ["1000-0-0", ValueError], + ["1000-1", ValueError], + ["1000-1-1", ValueError], + ["1", ValueError], + ["a", ValueError], + ["year", ValueError], + ["eternity", ValueError], + ["999", ValueError], + ["1:1000-01-01", ValueError], + ["a:1000-01-01", ValueError], + ["year:1000-01-01", ValueError], + ["year:1000-01-01:1", ValueError], + ["year:1000-01-01:3", ValueError], + ["1000-01-01:a", ValueError], + ["1000-01-01:1", ValueError], + [(), AssertionError], + [{}, AssertionError], + ["", ValueError], + [(None, None, None, None), AssertionError], + ]) +def test_instant_with_an_invalid_argument(arg, error): + with pytest.raises(error): + periods.instant(arg) + + +@pytest.mark.parametrize("arg, expected", [ + [None, None], + [Instant((1, 1, 1)), datetime.date(1, 1, 1)], + [Instant((4, 2, 29)), datetime.date(4, 2, 29)], + [(1, 1, 1), datetime.date(1, 1, 1)], + ]) +def test_instant_date_with_a_valid_argument(arg, expected): + assert periods.instant_date(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + [Instant((-1, 1, 1)), ValueError], + [Instant((1, -1, 1)), ValueError], + [Instant((1, 13, -1)), ValueError], + [Instant((1, 1, -1)), ValueError], + [Instant((1, 1, 32)), ValueError], + [Instant((1, 2, 29)), ValueError], + [Instant(("1", 1, 1)), TypeError], + [(1,), TypeError], + [(1, 1), TypeError], + ]) +def test_instant_date_with_an_invalid_argument(arg, error): + with pytest.raises(error): + periods.instant_date(arg) + + +@pytest.mark.parametrize("arg, expected", [ + ["eternity", Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], + ["ETERNITY", Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], + [periods.ETERNITY, Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], + [Instant((1, 1, 1)), Period((periods.DAY, Instant((1, 1, 1)), 1))], + [Period((periods.DAY, Instant((1, 1, 1)), 365)), Period((periods.DAY, Instant((1, 1, 1)), 365))], + [-1, Period((periods.YEAR, Instant((-1, 1, 1)), 1))], + [0, Period((periods.YEAR, Instant((0, 1, 1)), 1))], + [1, Period((periods.YEAR, Instant((1, 1, 1)), 1))], + [999, Period((periods.YEAR, Instant((999, 1, 1)), 1))], + [1000, Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["1000-1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["1000-1-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1004-02-29", Period((periods.DAY, Instant((1004, 2, 29)), 1))], + ["year:1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000-01:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], + ["year:1000-01:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], + ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["month:1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["month:1000-01-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["month:1000-01:1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["month:1000-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["day:1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["day:1000-01-01:3", Period((periods.DAY, Instant((1000, 1, 1)), 3))], + ]) +def test_period_with_a_valid_argument(arg, expected): + assert periods.period(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + [None, ValueError], + [periods.YEAR, ValueError], + [datetime.date(1, 1, 1), ValueError], + ["1000-0", ValueError], + ["1000-13", ValueError], + ["1000-0-0", ValueError], + ["1000-1-0", ValueError], + ["1000-2-31", ValueError], + ["1", ValueError], + ["a", ValueError], + ["year", ValueError], + ["999", ValueError], + ["1:1000", ValueError], + ["a:1000", ValueError], + ["month:1000", ValueError], + ["day:1000-01", ValueError], + ["1000:a", ValueError], + ["1000:1", ValueError], + ["1000-01:1", ValueError], + ["1000-01-01:1", ValueError], + ["month:1000:1", ValueError], + ["day:1000:1", ValueError], + ["day:1000-01:1", ValueError], + [(), ValueError], + [{}, ValueError], + ["", ValueError], + [(None,), ValueError], + [(None, None), ValueError], + [(None, None, None), ValueError], + [(None, None, None, None), ValueError], + [(datetime.date(1, 1, 1),), ValueError], + [(Instant((1, 1, 1)),), ValueError], + [(Period((periods.DAY, Instant((1, 1, 1)), 365)),), ValueError], + [(1,), ValueError], + [(1, 1), ValueError], + [(1, 1, 1), ValueError], + [(-1,), ValueError], + [(-1, -1), ValueError], + [(-1, -1, -1), ValueError], + [("-1",), ValueError], + [("-1", "-1"), ValueError], + [("-1", "-1", "-1"), ValueError], + [("1-1",), ValueError], + [("1-1-1",), ValueError], + ]) +def test_period_with_an_invalid_argument(arg, error): + with pytest.raises(error): + periods.period(arg) + + +@pytest.mark.parametrize("arg, expected", [ + ["1", None], + ["999", None], + ["1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["1000-1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], + ["1000-1-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1000-01-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1000-01-99", None], + ]) +def test__parse_simple_period_with_a_valid_argument(arg, expected): + assert helpers._parse_simple_period(arg) == expected + + +@pytest.mark.parametrize("arg, expected", [ + [Period((periods.DAY, Instant((1, 1, 1)), 365)), "100_365"], + [Period((periods.MONTH, Instant((1, 1, 1)), 12)), "200_12"], + [Period((periods.YEAR, Instant((1, 1, 1)), 2)), "300_2"], + [Period((periods.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"], + [(periods.DAY, None, 1), "100_1"], + [(periods.MONTH, None, -1000), "200_-1000"], + ]) +def test_key_period_size_with_a_valid_argument(arg, expected): + assert periods.key_period_size(arg) == expected diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py new file mode 100644 index 0000000000..4aab4d3339 --- /dev/null +++ b/openfisca_core/periods/tests/test_period.py @@ -0,0 +1,80 @@ +import pytest + +from openfisca_core import periods +from openfisca_core.periods import Instant, Period + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.YEAR, Instant((2022, 1, 1)), 1, "2022"], + [periods.MONTH, Instant((2022, 1, 1)), 12, "2022"], + [periods.YEAR, Instant((2022, 3, 1)), 1, "year:2022-03"], + [periods.MONTH, Instant((2022, 3, 1)), 12, "year:2022-03"], + [periods.YEAR, Instant((2022, 1, 1)), 3, "year:2022:3"], + [periods.YEAR, Instant((2022, 1, 3)), 3, "year:2022:3"], + ]) +def test_str_with_years(date_unit, instant, size, expected): + assert str(Period((date_unit, instant, size))) == expected + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.MONTH, Instant((2022, 1, 1)), 1, "2022-01"], + [periods.MONTH, Instant((2022, 1, 1)), 3, "month:2022-01:3"], + [periods.MONTH, Instant((2022, 3, 1)), 3, "month:2022-03:3"], + ]) +def test_str_with_months(date_unit, instant, size, expected): + assert str(Period((date_unit, instant, size))) == expected + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.DAY, Instant((2022, 1, 1)), 1, "2022-01-01"], + [periods.DAY, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], + [periods.DAY, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], + ]) +def test_str_with_days(date_unit, instant, size, expected): + assert str(Period((date_unit, instant, size))) == expected + + +@pytest.mark.parametrize("period, unit, length, first, last", [ + (periods.period('year:2014:2'), periods.YEAR, 2, periods.period('2014'), periods.period('2015')), + (periods.period(2017), periods.MONTH, 12, periods.period('2017-01'), periods.period('2017-12')), + (periods.period('year:2014:2'), periods.MONTH, 24, periods.period('2014-01'), periods.period('2015-12')), + (periods.period('month:2014-03:3'), periods.MONTH, 3, periods.period('2014-03'), periods.period('2014-05')), + (periods.period(2017), periods.DAY, 365, periods.period('2017-01-01'), periods.period('2017-12-31')), + (periods.period('year:2014:2'), periods.DAY, 730, periods.period('2014-01-01'), periods.period('2015-12-31')), + (periods.period('month:2014-03:3'), periods.DAY, 92, periods.period('2014-03-01'), periods.period('2014-05-31')), + ]) +def test_subperiods(period, unit, length, first, last): + subperiods = period.get_subperiods(unit) + assert len(subperiods) == length + assert subperiods[0] == first + assert subperiods[-1] == last + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.MONTH, Instant((2022, 12, 1)), 1, 1], + [periods.MONTH, Instant((2012, 2, 3)), 1, 1], + [periods.MONTH, Instant((2022, 1, 3)), 3, 3], + [periods.MONTH, Instant((2012, 1, 3)), 3, 3], + [periods.YEAR, Instant((2022, 12, 1)), 1, 12], + [periods.YEAR, Instant((2012, 1, 1)), 1, 12], + [periods.YEAR, Instant((2022, 1, 1)), 2, 24], + ]) +def test_day_size_in_months(date_unit, instant, size, expected): + period = Period((date_unit, instant, size)) + assert period.size_in_months == expected + + +@pytest.mark.parametrize("date_unit, instant, size, expected", [ + [periods.DAY, Instant((2022, 12, 31)), 1, 1], + [periods.DAY, Instant((2022, 12, 31)), 3, 3], + [periods.MONTH, Instant((2022, 12, 1)), 1, 31], + [periods.MONTH, Instant((2012, 2, 3)), 1, 29], + [periods.MONTH, Instant((2022, 1, 3)), 3, 31 + 28 + 31], + [periods.MONTH, Instant((2012, 1, 3)), 3, 31 + 29 + 31], + [periods.YEAR, Instant((2022, 12, 1)), 1, 365], + [periods.YEAR, Instant((2012, 1, 1)), 1, 366], + [periods.YEAR, Instant((2022, 1, 1)), 2, 730], + ]) +def test_day_size_in_days(date_unit, instant, size, expected): + period = Period((date_unit, instant, size)) + assert period.size_in_days == expected diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py deleted file mode 100644 index 0816a0ce2e..0000000000 --- a/tests/core/test_periods.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- - - -import pytest - -from openfisca_core.periods import YEAR, MONTH, DAY, 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) From fb6c119367a618b186829c5074ac31f0300994d3 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 1 Aug 2022 19:59:34 +0200 Subject: [PATCH 19/93] Revert "Fix flake8/pycodestyle dependency error" This reverts commit 7e4538aaf9034c8f6c008a6d9a7318b7c5e1d10d. --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 68d0b0f029..7d10342eaf 100644 --- a/setup.py +++ b/setup.py @@ -38,18 +38,18 @@ ] api_requirements = [ - 'markupsafe >= 2.0.1, < 2.1.0', - 'flask >= 1.1.4, < 2.0.0', + 'markupsafe == 2.0.1', # While flask revision < 2 + 'flask == 1.1.4', 'flask-cors == 3.0.10', 'gunicorn >= 20.0.0, < 21.0.0', - 'werkzeug >= 1.0.1, < 2.0.0', + 'werkzeug >= 1.0.0, < 2.0.0', ] dev_requirements = [ 'autopep8 >= 1.4.0, < 1.6.0', 'coverage == 6.0.2', 'darglint == 1.8.0', - 'flake8 >= 3.9.0, < 4.0.0', + 'flake8 >= 4.0.0, < 4.1.0', 'flake8-bugbear >= 19.3.0, < 20.0.0', 'flake8-docstrings == 1.6.0', 'flake8-print >= 3.1.0, < 4.0.0', From 52561741e050f0faa5c80968c267a55e60220c85 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 2 Aug 2022 09:33:30 +0200 Subject: [PATCH 20/93] Rationalise Instant doc & tests --- openfisca_core/periods/instant_.py | 106 ++++++++++++------- openfisca_core/periods/tests/test_instant.py | 27 +++++ 2 files changed, 94 insertions(+), 39 deletions(-) create mode 100644 openfisca_core/periods/tests/test_instant.py diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 63cc63636a..0b0ba56a94 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -11,35 +11,39 @@ class Instant(tuple): - """An instant in time (year, month, day). + """An instant in time (``year``, ``month``, ``day``). - An :class:`.Instant` represents the most atomic and indivisible - legislation's time unit. + An ``Instant`` represents the most atomic and indivisible + legislation's date unit. Current implementation considers this unit to be a day, so - :obj:`instants <.Instant>` can be thought of as "day dates". + ``instants`` can be thought of as "day dates". Args: - (tuple(tuple(int, int, int))): + (tuple(int, int, int)): The ``year``, ``month``, and ``day``, accordingly. Examples: >>> instant = Instant((2021, 9, 13)) - >>> repr(Instant) - "" + ``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)} - >>> list(instant) - [2021, 9, 13] + All the rest of the ``tuple`` protocols are inherited as well: >>> instant[0] 2021 @@ -53,33 +57,9 @@ class Instant(tuple): >>> instant == (2021, 9, 13) True - >>> instant != (2021, 9, 13) - False - >>> instant > (2020, 9, 13) True - >>> instant < (2020, 9, 13) - False - - >>> instant >= (2020, 9, 13) - True - - >>> instant <= (2020, 9, 13) - False - - >>> instant.year - 2021 - - >>> instant.month - 9 - - >>> instant.day - 13 - - >>> instant.date - datetime.date(2021, 9, 13) - >>> year, month, day = instant """ @@ -97,18 +77,66 @@ def __str__(self) -> str: @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] @property 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. + + """ + instant_date = config.date_by_instant_cache.get(self) if instant_date is None: @@ -120,16 +148,16 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: """Increments/decrements the given instant with offset units. Args: - offset: How much of ``unit`` to offset. - unit: What to offset + offset (str | int): How much of ``unit`` to offset. + unit (str): What to offset. Returns: - :obj:`.Instant`: A new :obj:`.Instant` in time. + Instant: A new one. Raises: - :exc:`AssertionError`: When ``unit`` is not a date unit. - :exc:`AssertionError`: When ``offset`` is not either ``first-of``, - ``last-of``, or any :obj:`int`. + AssertionError: When ``unit`` is not a date unit. + AssertionError: When ``offset`` is not either ``first-of``, + ``last-of``, or any ``int``. Examples: >>> Instant((2020, 12, 31)).offset("first-of", "month") diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py new file mode 100644 index 0000000000..5ad68cf88d --- /dev/null +++ b/openfisca_core/periods/tests/test_instant.py @@ -0,0 +1,27 @@ +import pytest + +from openfisca_core import periods +from openfisca_core.periods import Instant + + +@pytest.fixture +def instant(): + return Instant((2020, 2, 29)) + + +@pytest.mark.parametrize("offset, unit, expected", [ + ["first-of", periods.YEAR, Instant((2020, 1, 1))], + ["first-of", periods.MONTH, Instant((2020, 2, 1))], + ["first-of", periods.DAY, Instant((2020, 2, 29))], + ["last-of", periods.YEAR, Instant((2020, 12, 31))], + ["last-of", periods.MONTH, Instant((2020, 2, 29))], + ["last-of", periods.DAY, Instant((2020, 2, 29))], + [-3, periods.YEAR, Instant((2017, 2, 28))], + [-3, periods.MONTH, Instant((2019, 11, 29))], + [-3, periods.DAY, Instant((2020, 2, 26))], + [3, periods.YEAR, Instant((2023, 2, 28))], + [3, periods.MONTH, Instant((2020, 5, 29))], + [3, periods.DAY, Instant((2020, 3, 3))], + ]) +def test_offset(instant, offset, unit, expected): + assert expected == instant.offset(offset, unit) From 632566a4142393dadf4a62ca82ebc89c7de2d832 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 2 Aug 2022 11:51:27 +0200 Subject: [PATCH 21/93] Simplify periods' doc --- openfisca_core/periods/period_.py | 495 ++++++++----------- openfisca_core/periods/tests/test_helpers.py | 107 +--- openfisca_core/periods/tests/test_instant.py | 2 +- 3 files changed, 215 insertions(+), 389 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 20c301a8b7..4b8dc6437f 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -14,39 +14,44 @@ class Period(tuple): """Toolbox to handle date intervals. - A :class:`.Period` is a triple (``unit``, ``start``, ``size``). + A ``Period`` is a triple (``unit``, ``start``, ``size``). Attributes: - unit (:obj:`str`): + unit (str): Either ``year``, ``month``, ``day`` or ``eternity``. start (:obj:`.Instant`): The "instant" the :obj:`.Period` starts at. - size (:obj:`int`): + size (int): The amount of ``unit``, starting at ``start``, at least ``1``. Args: - (tuple(tuple(str, .Instant, int))): + tuple(str, .Instant, int))): The ``unit``, ``start``, and ``size``, accordingly. Examples: >>> instant = Instant((2021, 9, 1)) - >>> period = Period(("year", instant, 3)) + >>> period = Period((config.YEAR, instant, 3)) - >>> repr(Period) - "" + ``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]) + 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... - >>> list(period) - ['year', Instant((2021, 9, 1)), 3] + All the rest of the ``tuple`` protocols are inherited as well: >>> period[0] 'year' @@ -60,69 +65,10 @@ class Period(tuple): >>> period == Period(("year", instant, 3)) True - >>> period != Period(("year", instant, 3)) - False - >>> period > Period(("year", instant, 3)) False - >>> period < Period(("year", instant, 3)) - False - - >>> period >= Period(("year", instant, 3)) - True - - >>> period <= Period(("year", instant, 3)) - True - - >>> period.date - Traceback (most recent call last): - AssertionError: "date" is undefined for a period of size > 1 - - >>> Period(("year", instant, 1)).date - datetime.date(2021, 9, 1) - - >>> period.days - 1096 - - >>> period.size - 3 - - >>> period.size_in_months - 36 - - >>> period.size_in_days - 1096 - - >>> period.start - Instant((2021, 9, 1)) - - >>> period.stop - Instant((2024, 8, 31)) - - >>> period.unit - 'year' - - >>> period.last_3_months - Period(('month', Instant((2021, 6, 1)), 3)) - - >>> period.last_month - Period(('month', Instant((2021, 8, 1)), 1)) - - >>> period.last_year - Period(('year', Instant((2020, 1, 1)), 1)) - - >>> period.n_2 - Period(('year', Instant((2019, 1, 1)), 1)) - - >>> period.this_year - Period(('year', Instant((2021, 1, 1)), 1)) - - >>> period.first_month - Period(('month', Instant((2021, 9, 1)), 1)) - - >>> period.first_day - Period(('day', Instant((2021, 9, 1)), 1)) + >>> unit, (year, month, day), size = period Since a period is a triple it can be used as a dictionary key. @@ -197,279 +143,164 @@ def __str__(self) -> str: @property def date(self) -> datetime.date: - assert self.size == 1, '"date" is undefined for a period of size > 1: {}'.format(self) - return self.start.date + """The date representation of the ``period``'s' start date. - @property - def days(self) -> int: - """Count the number of days in period.""" - return (self.stop.date - self.start.date).days + 1 - - def get_subperiods(self, unit: str) -> Sequence[types.Period]: - """Return the list of all the periods of unit ``unit``. + Returns: + A datetime.date. Examples: - >>> period = Period(("year", Instant((2021, 1, 1)), 1)) - >>> period.get_subperiods("month") - [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((config.YEAR, instant, 1)) + >>> period.date + datetime.date(2021, 10, 1) - >>> period = Period(("year", Instant((2021, 1, 1)), 2)) - >>> period.get_subperiods("year") - [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] + >>> period = Period((config.YEAR, instant, 3)) + >>> period.date + Traceback (most recent call last): + ValueError: "date" is undefined for a period of size > 1: year:2021-09:3. """ - 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: Union[str, int], - unit: Optional[str] = None, - ) -> types.Period: - """Increment (or decrement) the given period with offset units. - - Examples: - >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1) - Period(('day', Instant((2021, 1, 2)), 365)) - - >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "day") - Period(('day', Instant((2021, 1, 2)), 365)) - - >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "month") - Period(('day', Instant((2021, 2, 1)), 365)) - - >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "year") - Period(('day', Instant((2022, 1, 1)), 365)) - - >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(1) - Period(('month', Instant((2021, 2, 1)), 12)) - - >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(1, "day") - Period(('month', Instant((2021, 1, 2)), 12)) - - >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(1, "month") - Period(('month', Instant((2021, 2, 1)), 12)) - - >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(1, "year") - Period(('month', Instant((2022, 1, 1)), 12)) - - >>> Period(("year", Instant((2021, 1, 1)), 1)).offset(1) - Period(('year', Instant((2022, 1, 1)), 1)) - - >>> Period(("year", Instant((2021, 1, 1)), 1)).offset(1, "day") - Period(('year', Instant((2021, 1, 2)), 1)) - - >>> Period(("year", Instant((2021, 1, 1)), 1)).offset(1, "month") - Period(('year', Instant((2021, 2, 1)), 1)) - - >>> Period(("year", Instant((2021, 1, 1)), 1)).offset(1, "year") - Period(('year', Instant((2022, 1, 1)), 1)) - - >>> Period(("day", Instant((2011, 2, 28)), 1)).offset(1) - Period(('day', Instant((2011, 3, 1)), 1)) - - >>> Period(("month", Instant((2011, 2, 28)), 1)).offset(1) - Period(('month', Instant((2011, 3, 28)), 1)) - - >>> Period(("year", Instant((2011, 2, 28)), 1)).offset(1) - Period(('year', Instant((2012, 2, 28)), 1)) - - >>> Period(("day", Instant((2011, 3, 1)), 1)).offset(-1) - Period(('day', Instant((2011, 2, 28)), 1)) - - >>> Period(("month", Instant((2011, 3, 1)), 1)).offset(-1) - Period(('month', Instant((2011, 2, 1)), 1)) - - >>> Period(("year", Instant((2011, 3, 1)), 1)).offset(-1) - Period(('year', Instant((2010, 3, 1)), 1)) - - >>> Period(("day", Instant((2014, 1, 30)), 1)).offset(3) - Period(('day', Instant((2014, 2, 2)), 1)) - - >>> Period(("month", Instant((2014, 1, 30)), 1)).offset(3) - Period(('month', Instant((2014, 4, 30)), 1)) - - >>> Period(("year", Instant((2014, 1, 30)), 1)).offset(3) - Period(('year', Instant((2017, 1, 30)), 1)) - - >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(-3) - Period(('day', Instant((2020, 12, 29)), 365)) - - >>> Period(("month", Instant((2021, 1, 1)), 12)).offset(-3) - Period(('month', Instant((2020, 10, 1)), 12)) - - >>> Period(("year", Instant((2014, 1, 1)), 1)).offset(-3) - Period(('year', Instant((2011, 1, 1)), 1)) - - >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("first-of", "month") - Period(('day', Instant((2014, 2, 1)), 1)) - - >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("first-of", "year") - Period(('day', Instant((2014, 1, 1)), 1)) - - >>> Period(("day", Instant((2014, 2, 3)), 4)).offset("first-of", "month") - Period(('day', Instant((2014, 2, 1)), 4)) - - >>> Period(("day", Instant((2014, 2, 3)), 4)).offset("first-of", "year") - Period(('day', Instant((2014, 1, 1)), 4)) - - >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("first-of") - Period(('month', Instant((2014, 2, 1)), 1)) - - >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("first-of", "month") - Period(('month', Instant((2014, 2, 1)), 1)) - - >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("first-of", "year") - Period(('month', Instant((2014, 1, 1)), 1)) - - >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("first-of") - Period(('month', Instant((2014, 2, 1)), 4)) - - >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("first-of", "month") - Period(('month', Instant((2014, 2, 1)), 4)) + if self.size != 1: + raise ValueError(f'"date" is undefined for a period of size > 1: {self}.') - >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("first-of", "year") - Period(('month', Instant((2014, 1, 1)), 4)) - - >>> Period(("year", Instant((2014, 1, 30)), 1)).offset("first-of") - Period(('year', Instant((2014, 1, 1)), 1)) - - >>> Period(("year", Instant((2014, 1, 30)), 1)).offset("first-of", "month") - Period(('year', Instant((2014, 1, 1)), 1)) - - >>> Period(("year", Instant((2014, 1, 30)), 1)).offset("first-of", "year") - Period(('year', Instant((2014, 1, 1)), 1)) - - >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("first-of") - Period(('year', Instant((2014, 1, 1)), 1)) - - >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("first-of", "month") - Period(('year', Instant((2014, 2, 1)), 1)) - - >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("first-of", "year") - Period(('year', Instant((2014, 1, 1)), 1)) - - >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("last-of", "month") - Period(('day', Instant((2014, 2, 28)), 1)) - - >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("last-of", "year") - Period(('day', Instant((2014, 12, 31)), 1)) + return self.start.date - >>> Period(("day", Instant((2014, 2, 3)), 4)).offset("last-of", "month") - Period(('day', Instant((2014, 2, 28)), 4)) + @property + def unit(self) -> str: + """The ``unit`` of the ``Period``. - >>> Period(("day", Instant((2014, 2, 3)), 4)).offset("last-of", "year") - Period(('day', Instant((2014, 12, 31)), 4)) + Returns: + An int. - >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("last-of") - Period(('month', Instant((2014, 2, 28)), 1)) + Example: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((config.YEAR, instant, 3)) + >>> period.unit + 'year' - >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("last-of", "month") - Period(('month', Instant((2014, 2, 28)), 1)) + """ - >>> Period(("month", Instant((2014, 2, 3)), 1)).offset("last-of", "year") - Period(('month', Instant((2014, 12, 31)), 1)) + return self[0] - >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of") - Period(('month', Instant((2014, 2, 28)), 4)) + @property + def days(self) -> int: + """Count the number of days in period. - >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of", "month") - Period(('month', Instant((2014, 2, 28)), 4)) + Returns: + An int. - >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of", "year") - Period(('month', Instant((2014, 12, 31)), 4)) + Examples: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((config.YEAR, instant, 3)) + >>> period.size_in_days + 1096 - >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of") - Period(('year', Instant((2014, 12, 31)), 1)) + >>> period = Period((config.MONTH, instant, 3)) + >>> period.size_in_days + 92 - >>> Period(("year", Instant((2014, 1, 1)), 1)).offset("last-of", "month") - Period(('year', Instant((2014, 1, 31)), 1)) + """ - >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of", "year") - Period(('year', Instant((2014, 12, 31)), 1)) + return (self.stop.date - self.start.date).days + 1 - >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of") - Period(('year', Instant((2014, 12, 31)), 1)) + @property + def size(self) -> int: + """The ``size`` of the ``Period``. - >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of", "month") - Period(('year', Instant((2014, 2, 28)), 1)) + Returns: + An int. - >>> Period(("year", Instant((2014, 2, 3)), 1)).offset("last-of", "year") - Period(('year', Instant((2014, 12, 31)), 1)) + Example: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((config.YEAR, instant, 3)) + >>> period.size + 3 """ - return self.__class__((self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2])) - - def contains(self, other: types.Period) -> bool: - """Returns ``True`` if the period contains ``other``. - - For instance, ``period(2015)`` contains ``period(2015-01)``. + return self[2] - """ + @property + def size_in_months(self) -> int: + """The ``size`` of the ``Period`` in months. - return self.start <= other.start and self.stop >= other.stop + Returns: + An int. - @property - def size(self) -> int: - """Return the size of the period.""" + Examples: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((config.YEAR, instant, 3)) + >>> period.size_in_months + 36 - return self[2] + >>> period = Period((config.DAY, instant, 3)) + >>> period.size_in_months + Traceback (most recent call last): + ValueError: Cannot calculate number of months in day. - @property - def size_in_months(self) -> int: - """Return the size of the period in months.""" + """ 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])) + + raise ValueError(f"Cannot calculate number of months in {self[0]}") @property def size_in_days(self) -> int: - """Return the size of the period in days.""" + """The ``size`` of the ``Period`` in days. + + Examples: + >>> instant = Instant((2019, 10, 1)) + >>> period = Period((config.YEAR, instant, 3)) + >>> period.size_in_days + 1096 + + >>> period = Period((config.MONTH, instant, 3)) + >>> period.size_in_days + 92 + + """ 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)) + raise ValueError(f"Cannot calculate number of days in {unit}") @property def start(self) -> types.Instant: - """Return the first day of the period as an Instant instance.""" + """The ``Instant`` at which the ``Period`` starts. + + Returns: + An Instant. + + Example: + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((config.YEAR, instant, 3)) + >>> period.start + Instant((2021, 10, 1)) + + """ return self[1] @property def stop(self) -> types.Instant: - """Return the last day of the period as an Instant instance. - - Examples: - >>> Period(("year", Instant((2022, 1, 1)), 1)).stop - Instant((2022, 12, 31)) - - >>> Period(("month", Instant((2022, 1, 1)), 12)).stop - Instant((2022, 12, 31)) + """Last day of the ``Period`` as an ``Instant``. - >>> Period(("day", Instant((2022, 1, 1)), 365)).stop - Instant((2022, 12, 31)) + Returns: + An Instant. + Examples: >>> Period(("year", Instant((2012, 2, 29)), 1)).stop Instant((2013, 2, 28)) @@ -479,15 +310,6 @@ def stop(self) -> types.Instant: >>> Period(("day", Instant((2012, 2, 29)), 1)).stop Instant((2012, 2, 29)) - >>> Period(("year", Instant((2012, 2, 29)), 2)).stop - Instant((2014, 2, 28)) - - >>> Period(("month", Instant((2012, 2, 29)), 2)).stop - Instant((2012, 4, 28)) - - >>> Period(("day", Instant((2012, 2, 29)), 2)).stop - Instant((2012, 3, 1)) - """ unit, start_instant, size = self @@ -531,12 +353,6 @@ def stop(self) -> types.Instant: day -= month_last_day return Instant((year, month, day)) - @property - def unit(self) -> str: - return self[0] - - # Reference periods - @property def last_month(self) -> types.Period: return self.first_month.offset(-1) @@ -569,3 +385,80 @@ def first_month(self) -> types.Period: @property def first_day(self) -> types.Period: return self.__class__((config.DAY, self.start, 1)) + + def get_subperiods(self, unit: str) -> Sequence[types.Period]: + """Return the list of all the periods of unit ``unit``. + + Examples: + >>> period = Period((config.YEAR, Instant((2021, 1, 1)), 1)) + >>> period.get_subperiods(config.MONTH) + [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] + + >>> period = Period((config.YEAR, Instant((2021, 1, 1)), 2)) + >>> period.get_subperiods(config.YEAR) + [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] + + """ + + 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: Union[str, int], + unit: Optional[str] = None, + ) -> types.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(("day", Instant((2014, 2, 3)), 1)).offset("first-of", "month") + Period(('day', Instant((2014, 2, 1)), 1)) + + >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of", "month") + Period(('month', Instant((2014, 2, 28)), 4)) + + >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(-3) + Period(('day', Instant((2020, 12, 29)), 365)) + + >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "year") + Period(('day', Instant((2022, 1, 1)), 365)) + + """ + + return self.__class__((self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2])) + + def contains(self, other: types.Period) -> bool: + """Checks if a ``period`` contains another one. + + Args: + other (:obj:`.Period`): The other ``Period``. + + Returns + True if ``other`` is contained, otherwise False. + + Example: + >>> period = Period((config.YEAR, Instant((2021, 1, 1)), 1)) + >>> sub_period = Period((config.MONTH, Instant((2021, 1, 1)), 3)) + + >>> period.contains(sub_period) + True + + """ + + return self.start <= other.start and self.stop >= other.stop diff --git a/openfisca_core/periods/tests/test_helpers.py b/openfisca_core/periods/tests/test_helpers.py index 6dd09788b9..01bb02c416 100644 --- a/openfisca_core/periods/tests/test_helpers.py +++ b/openfisca_core/periods/tests/test_helpers.py @@ -11,30 +11,12 @@ [datetime.date(1, 1, 1), Instant((1, 1, 1))], [Instant((1, 1, 1)), Instant((1, 1, 1))], [Period((periods.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))], - [-1, Instant((-1, 1, 1))], - [0, Instant((0, 1, 1))], - [1, Instant((1, 1, 1))], - [999, Instant((999, 1, 1))], [1000, Instant((1000, 1, 1))], ["1000", Instant((1000, 1, 1))], ["1000-01", Instant((1000, 1, 1))], ["1000-01-01", Instant((1000, 1, 1))], - [(None,), Instant((None, 1, 1))], - [(None, None), Instant((None, None, 1))], - [(None, None, None), Instant((None, None, None))], - [(datetime.date(1, 1, 1),), Instant((datetime.date(1, 1, 1), 1, 1))], - [(Instant((1, 1, 1)),), Instant((Instant((1, 1, 1)), 1, 1))], - [(Period((periods.DAY, Instant((1, 1, 1)), 365)),), Instant((Period((periods.DAY, Instant((1, 1, 1)), 365)), 1, 1))], - [(-1,), Instant((-1, 1, 1))], - [(-1, -1), Instant((-1, -1, 1))], - [(-1, -1, -1), Instant((-1, -1, -1))], - [("-1",), Instant(("-1", 1, 1))], - [("-1", "-1"), Instant(("-1", "-1", 1))], - [("-1", "-1", "-1"), Instant(("-1", "-1", "-1"))], - [("1-1",), Instant(("1-1", 1, 1))], - [("1-1-1",), Instant(("1-1-1", 1, 1))], ]) -def test_instant_with_a_valid_argument(arg, expected): +def test_instant(arg, expected): assert periods.instant(arg) == expected @@ -42,25 +24,18 @@ def test_instant_with_a_valid_argument(arg, expected): [periods.YEAR, ValueError], [periods.ETERNITY, ValueError], ["1000-0", ValueError], - ["1000-0-0", ValueError], ["1000-1", ValueError], + ["1000-13", ValueError], + ["1000-0-0", ValueError], ["1000-1-1", ValueError], - ["1", ValueError], - ["a", ValueError], - ["year", ValueError], - ["eternity", ValueError], - ["999", ValueError], - ["1:1000-01-01", ValueError], - ["a:1000-01-01", ValueError], + ["1000-01-0", ValueError], + ["1000-01-1", ValueError], + ["1000-01-32", ValueError], + ["month:1000", ValueError], + ["month:1000:1", ValueError], ["year:1000-01-01", ValueError], ["year:1000-01-01:1", ValueError], ["year:1000-01-01:3", ValueError], - ["1000-01-01:a", ValueError], - ["1000-01-01:1", ValueError], - [(), AssertionError], - [{}, AssertionError], - ["", ValueError], - [(None, None, None, None), AssertionError], ]) def test_instant_with_an_invalid_argument(arg, error): with pytest.raises(error): @@ -73,20 +48,17 @@ def test_instant_with_an_invalid_argument(arg, error): [Instant((4, 2, 29)), datetime.date(4, 2, 29)], [(1, 1, 1), datetime.date(1, 1, 1)], ]) -def test_instant_date_with_a_valid_argument(arg, expected): +def test_instant_date(arg, expected): assert periods.instant_date(arg) == expected @pytest.mark.parametrize("arg, error", [ [Instant((-1, 1, 1)), ValueError], [Instant((1, -1, 1)), ValueError], - [Instant((1, 13, -1)), ValueError], [Instant((1, 1, -1)), ValueError], + [Instant((1, 13, 1)), ValueError], [Instant((1, 1, 32)), ValueError], [Instant((1, 2, 29)), ValueError], - [Instant(("1", 1, 1)), TypeError], - [(1,), TypeError], - [(1, 1), TypeError], ]) def test_instant_date_with_an_invalid_argument(arg, error): with pytest.raises(error): @@ -99,10 +71,6 @@ def test_instant_date_with_an_invalid_argument(arg, error): [periods.ETERNITY, Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], [Instant((1, 1, 1)), Period((periods.DAY, Instant((1, 1, 1)), 1))], [Period((periods.DAY, Instant((1, 1, 1)), 365)), Period((periods.DAY, Instant((1, 1, 1)), 365))], - [-1, Period((periods.YEAR, Instant((-1, 1, 1)), 1))], - [0, Period((periods.YEAR, Instant((0, 1, 1)), 1))], - [1, Period((periods.YEAR, Instant((1, 1, 1)), 1))], - [999, Period((periods.YEAR, Instant((999, 1, 1)), 1))], [1000, Period((periods.YEAR, Instant((1000, 1, 1)), 1))], ["1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], ["1000-1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], @@ -111,24 +79,19 @@ def test_instant_date_with_an_invalid_argument(arg, error): ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], ["1004-02-29", Period((periods.DAY, Instant((1004, 2, 29)), 1))], ["year:1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01:1", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], ["year:1000:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], + ["year:1000-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], ["year:1000-01:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], - ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["year:1000-01-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], ["month:1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["month:1000-01-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["month:1000-01:1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], ["month:1000-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], - ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], + ["month:1000-01-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], ["day:1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], ["day:1000-01-01:3", Period((periods.DAY, Instant((1000, 1, 1)), 3))], ]) -def test_period_with_a_valid_argument(arg, expected): +def test_period(arg, expected): assert periods.period(arg) == expected @@ -136,47 +99,19 @@ def test_period_with_a_valid_argument(arg, expected): [None, ValueError], [periods.YEAR, ValueError], [datetime.date(1, 1, 1), ValueError], + ["1000:1", ValueError], ["1000-0", ValueError], ["1000-13", ValueError], + ["1000-01:1", ValueError], ["1000-0-0", ValueError], ["1000-1-0", ValueError], ["1000-2-31", ValueError], - ["1", ValueError], - ["a", ValueError], - ["year", ValueError], - ["999", ValueError], - ["1:1000", ValueError], - ["a:1000", ValueError], - ["month:1000", ValueError], - ["day:1000-01", ValueError], - ["1000:a", ValueError], - ["1000:1", ValueError], - ["1000-01:1", ValueError], ["1000-01-01:1", ValueError], + ["month:1000", ValueError], ["month:1000:1", ValueError], ["day:1000:1", ValueError], + ["day:1000-01", ValueError], ["day:1000-01:1", ValueError], - [(), ValueError], - [{}, ValueError], - ["", ValueError], - [(None,), ValueError], - [(None, None), ValueError], - [(None, None, None), ValueError], - [(None, None, None, None), ValueError], - [(datetime.date(1, 1, 1),), ValueError], - [(Instant((1, 1, 1)),), ValueError], - [(Period((periods.DAY, Instant((1, 1, 1)), 365)),), ValueError], - [(1,), ValueError], - [(1, 1), ValueError], - [(1, 1, 1), ValueError], - [(-1,), ValueError], - [(-1, -1), ValueError], - [(-1, -1, -1), ValueError], - [("-1",), ValueError], - [("-1", "-1"), ValueError], - [("-1", "-1", "-1"), ValueError], - [("1-1",), ValueError], - [("1-1-1",), ValueError], ]) def test_period_with_an_invalid_argument(arg, error): with pytest.raises(error): @@ -194,7 +129,7 @@ def test_period_with_an_invalid_argument(arg, error): ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], ["1000-01-99", None], ]) -def test__parse_simple_period_with_a_valid_argument(arg, expected): +def test__parse_simple_period(arg, expected): assert helpers._parse_simple_period(arg) == expected @@ -203,8 +138,6 @@ def test__parse_simple_period_with_a_valid_argument(arg, expected): [Period((periods.MONTH, Instant((1, 1, 1)), 12)), "200_12"], [Period((periods.YEAR, Instant((1, 1, 1)), 2)), "300_2"], [Period((periods.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"], - [(periods.DAY, None, 1), "100_1"], - [(periods.MONTH, None, -1000), "200_-1000"], ]) def test_key_period_size_with_a_valid_argument(arg, expected): assert periods.key_period_size(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 5ad68cf88d..31db048c93 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -24,4 +24,4 @@ def instant(): [3, periods.DAY, Instant((2020, 3, 3))], ]) def test_offset(instant, offset, unit, expected): - assert expected == instant.offset(offset, unit) + assert instant.offset(offset, unit) == expected From 45b1eb2248bfef70b45f1b9e06df874e64f8b9af Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 2 Aug 2022 12:21:29 +0200 Subject: [PATCH 22/93] Rationalise period's offset tests --- openfisca_core/periods/instant_.py | 2 +- openfisca_core/periods/period_.py | 4 ++ openfisca_core/periods/tests/test_period.py | 57 ++++++++++++++++----- openfisca_tasks/lint.mk | 2 + openfisca_tasks/test_code.mk | 1 + 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 0b0ba56a94..7c40a056b8 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -7,6 +7,7 @@ from openfisca_core import types +from .. import periods from . import config @@ -75,7 +76,6 @@ def __str__(self) -> str: return instant_str - @property def year(self) -> int: """The ``year`` of the ``Instant``. diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 4b8dc6437f..a118c347f8 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -314,8 +314,10 @@ def stop(self) -> types.Instant: 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 @@ -327,6 +329,7 @@ def stop(self) -> types.Instant: month = 1 day -= month_last_day month_last_day = calendar.monthrange(year, month)[1] + else: if unit == 'month': month += size @@ -351,6 +354,7 @@ def stop(self) -> types.Instant: year += 1 month = 1 day -= month_last_day + return Instant((year, month, day)) @property diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 4aab4d3339..16d20733b2 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -4,6 +4,11 @@ from openfisca_core.periods import Instant, Period +@pytest.fixture +def instant(): + return Instant((2022, 12, 31)) + + @pytest.mark.parametrize("date_unit, instant, size, expected", [ [periods.YEAR, Instant((2022, 1, 1)), 1, "2022"], [periods.MONTH, Instant((2022, 1, 1)), 12, "2022"], @@ -34,20 +39,48 @@ def test_str_with_days(date_unit, instant, size, expected): assert str(Period((date_unit, instant, size))) == expected -@pytest.mark.parametrize("period, unit, length, first, last", [ - (periods.period('year:2014:2'), periods.YEAR, 2, periods.period('2014'), periods.period('2015')), - (periods.period(2017), periods.MONTH, 12, periods.period('2017-01'), periods.period('2017-12')), - (periods.period('year:2014:2'), periods.MONTH, 24, periods.period('2014-01'), periods.period('2015-12')), - (periods.period('month:2014-03:3'), periods.MONTH, 3, periods.period('2014-03'), periods.period('2014-05')), - (periods.period(2017), periods.DAY, 365, periods.period('2017-01-01'), periods.period('2017-12-31')), - (periods.period('year:2014:2'), periods.DAY, 730, periods.period('2014-01-01'), periods.period('2015-12-31')), - (periods.period('month:2014-03:3'), periods.DAY, 92, periods.period('2014-03-01'), periods.period('2014-05-31')), +@pytest.mark.parametrize("period_unit, unit, start, cease, count", [ + [periods.YEAR, periods.YEAR, Instant((2022, 1, 1)), Instant((2024, 1, 1)), 3], + [periods.YEAR, periods.MONTH, Instant((2022, 12, 1)), Instant((2025, 11, 1)), 36], + [periods.YEAR, periods.DAY, Instant((2022, 12, 31)), Instant((2025, 12, 30)), 1096], + [periods.MONTH, periods.MONTH, Instant((2022, 12, 1)), Instant((2023, 2, 1)), 3], + [periods.MONTH, periods.DAY, Instant((2022, 12, 31)), Instant((2023, 3, 30)), 90], + [periods.DAY, periods.DAY, Instant((2022, 12, 31)), Instant((2023, 1, 2)), 3], ]) -def test_subperiods(period, unit, length, first, last): +def test_subperiods(instant, period_unit, unit, start, cease, count): + period = Period((period_unit, instant, 3)) subperiods = period.get_subperiods(unit) - assert len(subperiods) == length - assert subperiods[0] == first - assert subperiods[-1] == last + assert len(subperiods) == count + assert subperiods[0] == Period((unit, start, 1)) + assert subperiods[-1] == Period((unit, cease, 1)) + + +@pytest.mark.parametrize("period_unit, offset, unit, expected", [ + [periods.YEAR, "first-of", periods.YEAR, Period(('year', Instant((2022, 1, 1)), 3))], + [periods.YEAR, "first-of", periods.MONTH, Period(('year', Instant((2022, 12, 1)), 3))], + [periods.YEAR, "last-of", periods.YEAR, Period(('year', Instant((2022, 12, 31)), 3))], + [periods.YEAR, "last-of", periods.MONTH, Period(('year', Instant((2022, 12, 31)), 3))], + [periods.YEAR, -3, periods.YEAR, Period(('year', Instant((2019, 12, 31)), 3))], + [periods.YEAR, 1, periods.MONTH, Period(('year', Instant((2023, 1, 31)), 3))], + [periods.YEAR, 3, periods.DAY, Period(('year', Instant((2023, 1, 3)), 3))], + [periods.MONTH, "first-of", periods.YEAR, Period(('month', Instant((2022, 1, 1)), 3))], + [periods.MONTH, "first-of", periods.MONTH, Period(('month', Instant((2022, 12, 1)), 3))], + [periods.MONTH, "last-of", periods.YEAR, Period(('month', Instant((2022, 12, 31)), 3))], + [periods.MONTH, "last-of", periods.MONTH, Period(('month', Instant((2022, 12, 31)), 3))], + [periods.MONTH, -3, periods.YEAR, Period(('month', Instant((2019, 12, 31)), 3))], + [periods.MONTH, 1, periods.MONTH, Period(('month', Instant((2023, 1, 31)), 3))], + [periods.MONTH, 3, periods.DAY, Period(('month', Instant((2023, 1, 3)), 3))], + [periods.DAY, "first-of", periods.YEAR, Period(('day', Instant((2022, 1, 1)), 3))], + [periods.DAY, "first-of", periods.MONTH, Period(('day', Instant((2022, 12, 1)), 3))], + [periods.DAY, "last-of", periods.YEAR, Period(('day', Instant((2022, 12, 31)), 3))], + [periods.DAY, "last-of", periods.MONTH, Period(('day', Instant((2022, 12, 31)), 3))], + [periods.DAY, -3, periods.YEAR, Period(('day', Instant((2019, 12, 31)), 3))], + [periods.DAY, 1, periods.MONTH, Period(('day', Instant((2023, 1, 31)), 3))], + [periods.DAY, 3, periods.DAY, Period(('day', Instant((2023, 1, 3)), 3))], + ]) +def test_offset(instant, period_unit, offset, unit, expected): + period = Period((period_unit, instant, 3)) + assert period.offset(offset, unit) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ 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 \ From 01c1cd0b0168d16655df971683e1892d49c85b82 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 28 Jul 2022 21:52:03 +0200 Subject: [PATCH 23/93] Version & CHANGELOG bump --- CHANGELOG.md | 10 ++++++++++ openfisca_core/periods/helpers.py | 24 ++++++++++++++++++------ openfisca_core/periods/instant_.py | 23 ++++++++++++++++++++++- openfisca_core/periods/period_.py | 4 ++-- setup.py | 2 +- 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 601cdab211..bfde1fd9da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +# 39.0.0 [#1138](https://github.com/openfisca/openfisca-core/pull/1138) + +#### Breaking changes + +- Deprecate `periods.intersect`. + +#### Technical changes + +- Fix `openfisca_core.periods` doctests. + # 38.0.0 [#989](https://github.com/openfisca/openfisca-core/pull/989) #### New Features diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 994e126b40..6c575d2989 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -54,7 +54,10 @@ def instant(value: Any) -> Optional[types.Instant]: if isinstance(value, str): if not config.INSTANT_PATTERN.match(value): - raise ValueError(f"'{value}' is not a valid instant. Instants are described using the 'YYYY-MM-DD' format, for instance '2015-06-15'.") + raise ValueError( + f"'{value}' is not a valid instant. Instants are described" + "using the 'YYYY-MM-DD' format, for instance '2015-06-15'." + ) instant = Instant( int(fragment) @@ -186,7 +189,7 @@ def period(value: Any) -> types.Period: if ":" not in value: _raise_error(value) - components = value.split(':') + components = value.split(":") # Left-most component must be a valid unit unit = components[0] @@ -242,18 +245,24 @@ def _parse_simple_period(value: str) -> Optional[types.Period]: 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)) @@ -264,15 +273,18 @@ def _raise_error(value: str) -> NoReturn: Examples: >>> _raise_error("Oi mate!") Traceback (most recent call last): - ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: 'Oi mate!'. + ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: + 'Oi mate!'. Learn more about legal period formats in OpenFisca: + . """ 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:", + "Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got:", + f"'{value}'. Learn more about legal period formats in OpenFisca:", "." ]) + raise ValueError(message) @@ -302,7 +314,7 @@ def key_period_size(period: types.Period) -> str: unit, start, size = period - return '{}_{}'.format(unit_weight(unit), size) + return f"{unit_weight(unit)}_{size}" def unit_weights() -> Dict[str, int]: diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 7c40a056b8..dbb37678fe 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -7,7 +7,6 @@ from openfisca_core import types -from .. import periods from . import config @@ -76,6 +75,7 @@ def __str__(self) -> str: return instant_str + @property def year(self) -> int: """The ``year`` of the ``Instant``. @@ -175,56 +175,77 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: """ 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 diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index a118c347f8..40fe8ce91b 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -157,7 +157,7 @@ def date(self) -> datetime.date: >>> period = Period((config.YEAR, instant, 3)) >>> period.date Traceback (most recent call last): - ValueError: "date" is undefined for a period of size > 1: year:2021-09:3. + ValueError: "date" is undefined for a period of size > 1: year:2021-10:3. """ @@ -247,7 +247,7 @@ def size_in_months(self) -> int: if(self[0] == config.YEAR): return self[2] * 12 - raise ValueError(f"Cannot calculate number of months in {self[0]}") + raise ValueError(f"Cannot calculate number of months in {self[0]}.") @property def size_in_days(self) -> int: diff --git a/setup.py b/setup.py index 7d10342eaf..2355296ea0 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,7 @@ setup( name = 'OpenFisca-Core', - version = '38.0.0', + version = '39.0.0', author = 'OpenFisca Team', author_email = 'contact@openfisca.org', classifiers = [ From c5b2b1fcc2072f3401ff8c69b907826145d3b653 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 01:28:20 +0100 Subject: [PATCH 24/93] Reorder package --- openfisca_core/periods/__init__.py | 61 +++--- .../periods/{config.py => _config.py} | 0 .../periods/{helpers.py => _funcs.py} | 193 +++++++++--------- openfisca_core/periods/instant_.py | 26 +-- openfisca_core/periods/period_.py | 96 ++++----- openfisca_core/periods/tests/test_helpers.py | 103 +++++----- openfisca_core/periods/tests/test_instant.py | 27 ++- openfisca_core/periods/tests/test_period.py | 131 ++++++------ 8 files changed, 320 insertions(+), 317 deletions(-) rename openfisca_core/periods/{config.py => _config.py} (100%) rename openfisca_core/periods/{helpers.py => _funcs.py} (87%) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 8acddd62c9..7c229e4e79 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -1,27 +1,33 @@ -# 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 +"""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 ( # noqa: F401 DAY, MONTH, YEAR, @@ -32,13 +38,14 @@ year_or_month_or_day_re, ) -from .helpers import ( # noqa: F401 +from ._funcs import ( # noqa: F401 instant, instant_date, - period, key_period_size, - unit_weights, + parse_simple_period, + period, unit_weight, + unit_weights, ) from .instant_ import Instant # noqa: F401 diff --git a/openfisca_core/periods/config.py b/openfisca_core/periods/_config.py similarity index 100% rename from openfisca_core/periods/config.py rename to openfisca_core/periods/_config.py diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/_funcs.py similarity index 87% rename from openfisca_core/periods/helpers.py rename to openfisca_core/periods/_funcs.py index 6c575d2989..5299829bd3 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/_funcs.py @@ -7,7 +7,7 @@ from openfisca_core import types -from . import config +from . import _config from .instant_ import Instant from .period_ import Period @@ -53,7 +53,7 @@ def instant(value: Any) -> Optional[types.Instant]: return value if isinstance(value, str): - if not config.INSTANT_PATTERN.match(value): + if not _config.INSTANT_PATTERN.match(value): raise ValueError( f"'{value}' is not a valid instant. Instants are described" "using the 'YYYY-MM-DD' format, for instance '2015-06-15'." @@ -111,14 +111,83 @@ def instant_date(instant: Optional[types.Instant]) -> Optional[datetime.date]: if instant is None: return None - instant_date = config.date_by_instant_cache.get(instant) + 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) + _config.date_by_instant_cache[instant] = instant_date = datetime.date(*instant) return instant_date +def key_period_size(period: types.Period) -> str: + """Define a key in order to sort periods by length. + + It uses two aspects: first, ``unit``, then, ``size``. + + Args: + period: An :mod:`.openfisca_core` :obj:`.Period`. + + Returns: + :obj:`str`: A string. + + Examples: + >>> instant = Instant((2021, 9, 14)) + + >>> period = Period(("day", instant, 1)) + >>> key_period_size(period) + '100_1' + + >>> period = Period(("year", instant, 3)) + >>> key_period_size(period) + '300_3' + + """ + + unit, start, size = period + + return f"{unit_weight(unit)}_{size}" + + +def parse_simple_period(value: str) -> Optional[types.Period]: + """Parse simple periods respecting the ISO format. + + Such as "2012" or "2015-03". + + Examples: + >>> parse_simple_period("2022") + Period(('year', Instant((2022, 1, 1)), 1)) + + >>> parse_simple_period("2022-02") + Period(('month', Instant((2022, 2, 1)), 1)) + + >>> parse_simple_period("2022-02-13") + Period(('day', Instant((2022, 2, 13)), 1)) + + """ + + 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 period(value: Any) -> types.Period: """Build a new period, aka a triple (unit, start_instant, size). @@ -168,19 +237,19 @@ def period(value: Any) -> types.Period: return value if isinstance(value, Instant): - return Period((config.DAY, value, 1)) + return Period((_config.DAY, value, 1)) - if value == "ETERNITY" or value == config.ETERNITY: + if value == "ETERNITY" or value == _config.ETERNITY: return Period(("eternity", instant(datetime.date.min), float("inf"))) if isinstance(value, int): - return Period((config.YEAR, Instant((value, 1, 1)), 1)) + 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) + period = parse_simple_period(value) if period is not None: return period @@ -194,11 +263,11 @@ def period(value: Any) -> types.Period: # Left-most component must be a valid unit unit = components[0] - if unit not in (config.DAY, config.MONTH, config.YEAR): + 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]) + base_period = parse_simple_period(components[1]) if not base_period: _raise_error(value) @@ -226,45 +295,33 @@ def period(value: Any) -> types.Period: return Period((unit, base_period.start, size)) -def _parse_simple_period(value: str) -> Optional[types.Period]: - """Parse simple periods respecting the ISO format. - - Such as "2012" or "2015-03". +def unit_weights() -> Dict[str, int]: + """Assign weights to date units. Examples: - >>> _parse_simple_period("2022") - Period(('year', Instant((2022, 1, 1)), 1)) - - >>> _parse_simple_period("2022-02") - Period(('month', Instant((2022, 2, 1)), 1)) - - >>> _parse_simple_period("2022-02-13") - Period(('day', Instant((2022, 2, 13)), 1)) + >>> unit_weights() + {'day': 100, ...} """ - try: - date = datetime.datetime.strptime(value, '%Y') - - except ValueError: - try: - date = datetime.datetime.strptime(value, '%Y-%m') + return { + _config.DAY: 100, + _config.MONTH: 200, + _config.YEAR: 300, + _config.ETERNITY: 400, + } - except ValueError: - try: - date = datetime.datetime.strptime(value, '%Y-%m-%d') - except ValueError: - return None +def unit_weight(unit: str) -> int: + """Retrieves a specific date unit weight. - else: - return Period((config.DAY, Instant((date.year, date.month, date.day)), 1)) + Examples: + >>> unit_weight("day") + 100 - else: - return Period((config.MONTH, Instant((date.year, date.month, 1)), 1)) + """ - else: - return Period((config.YEAR, Instant((date.year, date.month, 1)), 1)) + return unit_weights()[unit] def _raise_error(value: str) -> NoReturn: @@ -286,61 +343,3 @@ def _raise_error(value: str) -> NoReturn: ]) raise ValueError(message) - - -def key_period_size(period: types.Period) -> str: - """Define a key in order to sort periods by length. - - It uses two aspects: first, ``unit``, then, ``size``. - - Args: - period: An :mod:`.openfisca_core` :obj:`.Period`. - - Returns: - :obj:`str`: A string. - - Examples: - >>> instant = Instant((2021, 9, 14)) - - >>> period = Period(("day", instant, 1)) - >>> key_period_size(period) - '100_1' - - >>> period = Period(("year", instant, 3)) - >>> key_period_size(period) - '300_3' - - """ - - unit, start, size = period - - return f"{unit_weight(unit)}_{size}" - - -def unit_weights() -> Dict[str, int]: - """Assign weights to date units. - - Examples: - >>> unit_weights() - {'day': 100, ...} - - """ - - return { - config.DAY: 100, - config.MONTH: 200, - config.YEAR: 300, - config.ETERNITY: 400, - } - - -def unit_weight(unit: str) -> int: - """Retrieves a specific date unit weight. - - Examples: - >>> unit_weight("day") - 100 - - """ - - return unit_weights()[unit] diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index dbb37678fe..0a1f677006 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -7,7 +7,7 @@ from openfisca_core import types -from . import config +from . import _config class Instant(tuple): @@ -68,10 +68,10 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}({super(Instant, self).__repr__()})" def __str__(self) -> str: - instant_str = config.str_by_instant_cache.get(self) + 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() + _config.str_by_instant_cache[self] = instant_str = self.date.isoformat() return instant_str @@ -137,10 +137,10 @@ def date(self) -> datetime.date: """ - instant_date = config.date_by_instant_cache.get(self) + 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) + _config.date_by_instant_cache[self] = instant_date = datetime.date(*self) return instant_date @@ -176,28 +176,28 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: year, month, day = self - assert unit in (config.DAY, config.MONTH, config.YEAR), 'Invalid unit: {} of type {}'.format(unit, type(unit)) + 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: + if unit == _config.MONTH: day = 1 - elif unit == config.YEAR: + elif unit == _config.YEAR: month = 1 day = 1 elif offset == 'last-of': - if unit == config.MONTH: + if unit == _config.MONTH: day = calendar.monthrange(year, month)[1] - elif unit == config.YEAR: + 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: + if unit == _config.DAY: day += offset if offset < 0: @@ -223,7 +223,7 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: day -= month_last_day month_last_day = calendar.monthrange(year, month)[1] - elif unit == config.MONTH: + elif unit == _config.MONTH: month += offset if offset < 0: @@ -240,7 +240,7 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: if day > month_last_day: day = month_last_day - elif unit == config.YEAR: + elif unit == _config.YEAR: year += offset # Handle february month of leap year. diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 40fe8ce91b..a858f0abe8 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -7,7 +7,7 @@ from openfisca_core import types -from . import config, helpers +from . import _config, _funcs from .instant_ import Instant @@ -30,7 +30,7 @@ class Period(tuple): Examples: >>> instant = Instant((2021, 9, 1)) - >>> period = Period((config.YEAR, instant, 3)) + >>> period = Period((_config.YEAR, instant, 3)) ``Periods`` are represented as a ``tuple`` containing the ``unit``, an ``Instant`` and the ``size``: @@ -112,27 +112,27 @@ def __str__(self) -> str: unit, start_instant, size = self - if unit == config.ETERNITY: + 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 (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) + return '{}:{}-{:02d}'.format(_config.YEAR, year, month) # simple month - if unit == config.MONTH and size == 1: + if unit == _config.MONTH and size == 1: return '{}-{:02d}'.format(year, month) # several civil years - if unit == config.YEAR and month == 1: + if unit == _config.YEAR and month == 1: return '{}:{}:{}'.format(unit, year, size) - if unit == config.DAY: + if unit == _config.DAY: if size == 1: return '{}-{:02d}-{:02d}'.format(year, month, day) else: @@ -150,11 +150,11 @@ def date(self) -> datetime.date: Examples: >>> instant = Instant((2021, 10, 1)) - >>> period = Period((config.YEAR, instant, 1)) + >>> period = Period((_config.YEAR, instant, 1)) >>> period.date datetime.date(2021, 10, 1) - >>> period = Period((config.YEAR, instant, 3)) + >>> period = Period((_config.YEAR, instant, 3)) >>> period.date Traceback (most recent call last): ValueError: "date" is undefined for a period of size > 1: year:2021-10:3. @@ -175,7 +175,7 @@ def unit(self) -> str: Example: >>> instant = Instant((2021, 10, 1)) - >>> period = Period((config.YEAR, instant, 3)) + >>> period = Period((_config.YEAR, instant, 3)) >>> period.unit 'year' @@ -192,11 +192,11 @@ def days(self) -> int: Examples: >>> instant = Instant((2021, 10, 1)) - >>> period = Period((config.YEAR, instant, 3)) + >>> period = Period((_config.YEAR, instant, 3)) >>> period.size_in_days 1096 - >>> period = Period((config.MONTH, instant, 3)) + >>> period = Period((_config.MONTH, instant, 3)) >>> period.size_in_days 92 @@ -213,7 +213,7 @@ def size(self) -> int: Example: >>> instant = Instant((2021, 10, 1)) - >>> period = Period((config.YEAR, instant, 3)) + >>> period = Period((_config.YEAR, instant, 3)) >>> period.size 3 @@ -230,21 +230,21 @@ def size_in_months(self) -> int: Examples: >>> instant = Instant((2021, 10, 1)) - >>> period = Period((config.YEAR, instant, 3)) + >>> period = Period((_config.YEAR, instant, 3)) >>> period.size_in_months 36 - >>> period = Period((config.DAY, instant, 3)) + >>> period = Period((_config.DAY, instant, 3)) >>> period.size_in_months Traceback (most recent call last): ValueError: Cannot calculate number of months in day. """ - if (self[0] == config.MONTH): + if (self[0] == _config.MONTH): return self[2] - if(self[0] == config.YEAR): + if(self[0] == _config.YEAR): return self[2] * 12 raise ValueError(f"Cannot calculate number of months in {self[0]}.") @@ -255,11 +255,11 @@ def size_in_days(self) -> int: Examples: >>> instant = Instant((2019, 10, 1)) - >>> period = Period((config.YEAR, instant, 3)) + >>> period = Period((_config.YEAR, instant, 3)) >>> period.size_in_days 1096 - >>> period = Period((config.MONTH, instant, 3)) + >>> period = Period((_config.MONTH, instant, 3)) >>> period.size_in_days 92 @@ -267,11 +267,11 @@ def size_in_days(self) -> int: unit, instant, length = self - if unit == config.DAY: + if unit == _config.DAY: return length - if unit in [config.MONTH, config.YEAR]: - last_day = self.start.offset(length, unit).offset(-1, config.DAY) + 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(f"Cannot calculate number of days in {unit}") @@ -285,7 +285,7 @@ def start(self) -> types.Instant: Example: >>> instant = Instant((2021, 10, 1)) - >>> period = Period((config.YEAR, instant, 3)) + >>> period = Period((_config.YEAR, instant, 3)) >>> period.start Instant((2021, 10, 1)) @@ -315,7 +315,7 @@ def stop(self) -> types.Instant: unit, start_instant, size = self year, month, day = start_instant - if unit == config.ETERNITY: + if unit == _config.ETERNITY: return Instant((float("inf"), float("inf"), float("inf"))) if unit == 'day': @@ -364,57 +364,57 @@ def last_month(self) -> types.Period: @property def last_3_months(self) -> types.Period: start: types.Instant = self.first_month.start - return self.__class__((config.MONTH, start, 3)).offset(-3) + return self.__class__((_config.MONTH, start, 3)).offset(-3) @property def last_year(self) -> types.Period: - start: types.Instant = self.start.offset("first-of", config.YEAR) - return self.__class__((config.YEAR, start, 1)).offset(-1) + start: types.Instant = self.start.offset("first-of", _config.YEAR) + return self.__class__((_config.YEAR, start, 1)).offset(-1) @property def n_2(self) -> types.Period: - start: types.Instant = self.start.offset("first-of", config.YEAR) - return self.__class__((config.YEAR, start, 1)).offset(-2) + start: types.Instant = self.start.offset("first-of", _config.YEAR) + return self.__class__((_config.YEAR, start, 1)).offset(-2) @property def this_year(self) -> types.Period: - start: types.Instant = self.start.offset("first-of", config.YEAR) - return self.__class__((config.YEAR, start, 1)) + start: types.Instant = self.start.offset("first-of", _config.YEAR) + return self.__class__((_config.YEAR, start, 1)) @property def first_month(self) -> types.Period: - start: types.Instant = self.start.offset("first-of", config.MONTH) - return self.__class__((config.MONTH, start, 1)) + start: types.Instant = self.start.offset("first-of", _config.MONTH) + return self.__class__((_config.MONTH, start, 1)) @property def first_day(self) -> types.Period: - return self.__class__((config.DAY, self.start, 1)) + return self.__class__((_config.DAY, self.start, 1)) def get_subperiods(self, unit: str) -> Sequence[types.Period]: """Return the list of all the periods of unit ``unit``. Examples: - >>> period = Period((config.YEAR, Instant((2021, 1, 1)), 1)) - >>> period.get_subperiods(config.MONTH) + >>> period = Period((_config.YEAR, Instant((2021, 1, 1)), 1)) + >>> period.get_subperiods(_config.MONTH) [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] - >>> period = Period((config.YEAR, Instant((2021, 1, 1)), 2)) - >>> period.get_subperiods(config.YEAR) + >>> period = Period((_config.YEAR, Instant((2021, 1, 1)), 2)) + >>> period.get_subperiods(_config.YEAR) [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] """ - if helpers.unit_weight(self.unit) < helpers.unit_weight(unit): + if _funcs.unit_weight(self.unit) < _funcs.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.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.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)] + if unit == _config.DAY: + return [self.first_day.offset(i, _config.DAY) for i in range(self.size_in_days)] def offset( self, @@ -457,8 +457,8 @@ def contains(self, other: types.Period) -> bool: True if ``other`` is contained, otherwise False. Example: - >>> period = Period((config.YEAR, Instant((2021, 1, 1)), 1)) - >>> sub_period = Period((config.MONTH, Instant((2021, 1, 1)), 3)) + >>> period = Period((_config.YEAR, Instant((2021, 1, 1)), 1)) + >>> sub_period = Period((_config.MONTH, Instant((2021, 1, 1)), 3)) >>> period.contains(sub_period) True diff --git a/openfisca_core/periods/tests/test_helpers.py b/openfisca_core/periods/tests/test_helpers.py index 01bb02c416..149b0a82fa 100644 --- a/openfisca_core/periods/tests/test_helpers.py +++ b/openfisca_core/periods/tests/test_helpers.py @@ -3,18 +3,17 @@ import pytest from openfisca_core import periods -from openfisca_core.periods import Instant, Period, helpers @pytest.mark.parametrize("arg, expected", [ [None, None], - [datetime.date(1, 1, 1), Instant((1, 1, 1))], - [Instant((1, 1, 1)), Instant((1, 1, 1))], - [Period((periods.DAY, Instant((1, 1, 1)), 365)), Instant((1, 1, 1))], - [1000, Instant((1000, 1, 1))], - ["1000", Instant((1000, 1, 1))], - ["1000-01", Instant((1000, 1, 1))], - ["1000-01-01", Instant((1000, 1, 1))], + [datetime.date(1, 1, 1), periods.Instant((1, 1, 1))], + [periods.Instant((1, 1, 1)), periods.Instant((1, 1, 1))], + [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Instant((1, 1, 1))], + [1000, periods.Instant((1000, 1, 1))], + ["1000", periods.Instant((1000, 1, 1))], + ["1000-01", periods.Instant((1000, 1, 1))], + ["1000-01-01", periods.Instant((1000, 1, 1))], ]) def test_instant(arg, expected): assert periods.instant(arg) == expected @@ -44,8 +43,8 @@ def test_instant_with_an_invalid_argument(arg, error): @pytest.mark.parametrize("arg, expected", [ [None, None], - [Instant((1, 1, 1)), datetime.date(1, 1, 1)], - [Instant((4, 2, 29)), datetime.date(4, 2, 29)], + [periods.Instant((1, 1, 1)), datetime.date(1, 1, 1)], + [periods.Instant((4, 2, 29)), datetime.date(4, 2, 29)], [(1, 1, 1), datetime.date(1, 1, 1)], ]) def test_instant_date(arg, expected): @@ -53,12 +52,12 @@ def test_instant_date(arg, expected): @pytest.mark.parametrize("arg, error", [ - [Instant((-1, 1, 1)), ValueError], - [Instant((1, -1, 1)), ValueError], - [Instant((1, 1, -1)), ValueError], - [Instant((1, 13, 1)), ValueError], - [Instant((1, 1, 32)), ValueError], - [Instant((1, 2, 29)), ValueError], + [periods.Instant((-1, 1, 1)), ValueError], + [periods.Instant((1, -1, 1)), ValueError], + [periods.Instant((1, 1, -1)), ValueError], + [periods.Instant((1, 13, 1)), ValueError], + [periods.Instant((1, 1, 32)), ValueError], + [periods.Instant((1, 2, 29)), ValueError], ]) def test_instant_date_with_an_invalid_argument(arg, error): with pytest.raises(error): @@ -66,30 +65,30 @@ def test_instant_date_with_an_invalid_argument(arg, error): @pytest.mark.parametrize("arg, expected", [ - ["eternity", Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], - ["ETERNITY", Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], - [periods.ETERNITY, Period((periods.ETERNITY, Instant((1, 1, 1)), float("inf")))], - [Instant((1, 1, 1)), Period((periods.DAY, Instant((1, 1, 1)), 1))], - [Period((periods.DAY, Instant((1, 1, 1)), 365)), Period((periods.DAY, Instant((1, 1, 1)), 365))], - [1000, Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["1000-1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["1000-1-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["1004-02-29", Period((periods.DAY, Instant((1004, 2, 29)), 1))], - ["year:1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], - ["year:1000-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], - ["year:1000-01-01", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01:3", Period((periods.YEAR, Instant((1000, 1, 1)), 3))], - ["month:1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["month:1000-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], - ["month:1000-01-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["month:1000-01-01:3", Period((periods.MONTH, Instant((1000, 1, 1)), 3))], - ["day:1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["day:1000-01-01:3", Period((periods.DAY, Instant((1000, 1, 1)), 3))], + ["eternity", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), float("inf")))], + ["ETERNITY", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), float("inf")))], + [periods.ETERNITY, periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), float("inf")))], + [periods.Instant((1, 1, 1)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 1))], + [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365))], + [1000, periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["1000-1", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["1000-1-1", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["1004-02-29", periods.Period((periods.DAY, periods.Instant((1004, 2, 29)), 1))], + ["year:1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["year:1000:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], + ["year:1000-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["year:1000-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], + ["year:1000-01-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["year:1000-01-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], + ["month:1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["month:1000-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], + ["month:1000-01-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["month:1000-01-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], + ["day:1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["day:1000-01-01:3", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 3))], ]) def test_period(arg, expected): assert periods.period(arg) == expected @@ -121,23 +120,23 @@ def test_period_with_an_invalid_argument(arg, error): @pytest.mark.parametrize("arg, expected", [ ["1", None], ["999", None], - ["1000", Period((periods.YEAR, Instant((1000, 1, 1)), 1))], - ["1000-1", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["1000-01", Period((periods.MONTH, Instant((1000, 1, 1)), 1))], - ["1000-1-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["1000-01-1", Period((periods.DAY, Instant((1000, 1, 1)), 1))], - ["1000-01-01", Period((periods.DAY, Instant((1000, 1, 1)), 1))], + ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["1000-1", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["1000-1-1", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["1000-01-1", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], ["1000-01-99", None], ]) -def test__parse_simple_period(arg, expected): - assert helpers._parse_simple_period(arg) == expected +def testparse_simple_period(arg, expected): + assert periods.parse_simple_period(arg) == expected @pytest.mark.parametrize("arg, expected", [ - [Period((periods.DAY, Instant((1, 1, 1)), 365)), "100_365"], - [Period((periods.MONTH, Instant((1, 1, 1)), 12)), "200_12"], - [Period((periods.YEAR, Instant((1, 1, 1)), 2)), "300_2"], - [Period((periods.ETERNITY, Instant((1, 1, 1)), 1)), "400_1"], + [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), "100_365"], + [periods.Period((periods.MONTH, periods.Instant((1, 1, 1)), 12)), "200_12"], + [periods.Period((periods.YEAR, periods.Instant((1, 1, 1)), 2)), "300_2"], + [periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1)), "400_1"], ]) def test_key_period_size_with_a_valid_argument(arg, expected): assert periods.key_period_size(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 31db048c93..a0c098cc32 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -1,27 +1,26 @@ import pytest from openfisca_core import periods -from openfisca_core.periods import Instant @pytest.fixture def instant(): - return Instant((2020, 2, 29)) + return periods.Instant((2020, 2, 29)) @pytest.mark.parametrize("offset, unit, expected", [ - ["first-of", periods.YEAR, Instant((2020, 1, 1))], - ["first-of", periods.MONTH, Instant((2020, 2, 1))], - ["first-of", periods.DAY, Instant((2020, 2, 29))], - ["last-of", periods.YEAR, Instant((2020, 12, 31))], - ["last-of", periods.MONTH, Instant((2020, 2, 29))], - ["last-of", periods.DAY, Instant((2020, 2, 29))], - [-3, periods.YEAR, Instant((2017, 2, 28))], - [-3, periods.MONTH, Instant((2019, 11, 29))], - [-3, periods.DAY, Instant((2020, 2, 26))], - [3, periods.YEAR, Instant((2023, 2, 28))], - [3, periods.MONTH, Instant((2020, 5, 29))], - [3, periods.DAY, Instant((2020, 3, 3))], + ["first-of", periods.YEAR, periods.Instant((2020, 1, 1))], + ["first-of", periods.MONTH, periods.Instant((2020, 2, 1))], + ["first-of", periods.DAY, periods.Instant((2020, 2, 29))], + ["last-of", periods.YEAR, periods.Instant((2020, 12, 31))], + ["last-of", periods.MONTH, periods.Instant((2020, 2, 29))], + ["last-of", periods.DAY, periods.Instant((2020, 2, 29))], + [-3, periods.YEAR, periods.Instant((2017, 2, 28))], + [-3, periods.MONTH, periods.Instant((2019, 11, 29))], + [-3, periods.DAY, periods.Instant((2020, 2, 26))], + [3, periods.YEAR, periods.Instant((2023, 2, 28))], + [3, periods.MONTH, periods.Instant((2020, 5, 29))], + [3, periods.DAY, periods.Instant((2020, 3, 3))], ]) def test_offset(instant, offset, unit, expected): assert instant.offset(offset, unit) == expected diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 16d20733b2..723c806ad3 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -1,113 +1,112 @@ import pytest from openfisca_core import periods -from openfisca_core.periods import Instant, Period @pytest.fixture def instant(): - return Instant((2022, 12, 31)) + return periods.Instant((2022, 12, 31)) @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.YEAR, Instant((2022, 1, 1)), 1, "2022"], - [periods.MONTH, Instant((2022, 1, 1)), 12, "2022"], - [periods.YEAR, Instant((2022, 3, 1)), 1, "year:2022-03"], - [periods.MONTH, Instant((2022, 3, 1)), 12, "year:2022-03"], - [periods.YEAR, Instant((2022, 1, 1)), 3, "year:2022:3"], - [periods.YEAR, Instant((2022, 1, 3)), 3, "year:2022:3"], + [periods.YEAR, periods.Instant((2022, 1, 1)), 1, "2022"], + [periods.MONTH, periods.Instant((2022, 1, 1)), 12, "2022"], + [periods.YEAR, periods.Instant((2022, 3, 1)), 1, "year:2022-03"], + [periods.MONTH, periods.Instant((2022, 3, 1)), 12, "year:2022-03"], + [periods.YEAR, periods.Instant((2022, 1, 1)), 3, "year:2022:3"], + [periods.YEAR, periods.Instant((2022, 1, 3)), 3, "year:2022:3"], ]) def test_str_with_years(date_unit, instant, size, expected): - assert str(Period((date_unit, instant, size))) == expected + assert str(periods.Period((date_unit, instant, size))) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, Instant((2022, 1, 1)), 1, "2022-01"], - [periods.MONTH, Instant((2022, 1, 1)), 3, "month:2022-01:3"], - [periods.MONTH, Instant((2022, 3, 1)), 3, "month:2022-03:3"], + [periods.MONTH, periods.Instant((2022, 1, 1)), 1, "2022-01"], + [periods.MONTH, periods.Instant((2022, 1, 1)), 3, "month:2022-01:3"], + [periods.MONTH, periods.Instant((2022, 3, 1)), 3, "month:2022-03:3"], ]) def test_str_with_months(date_unit, instant, size, expected): - assert str(Period((date_unit, instant, size))) == expected + assert str(periods.Period((date_unit, instant, size))) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.DAY, Instant((2022, 1, 1)), 1, "2022-01-01"], - [periods.DAY, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], - [periods.DAY, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], + [periods.DAY, periods.Instant((2022, 1, 1)), 1, "2022-01-01"], + [periods.DAY, periods.Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], + [periods.DAY, periods.Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], ]) def test_str_with_days(date_unit, instant, size, expected): - assert str(Period((date_unit, instant, size))) == expected + assert str(periods.Period((date_unit, instant, size))) == expected @pytest.mark.parametrize("period_unit, unit, start, cease, count", [ - [periods.YEAR, periods.YEAR, Instant((2022, 1, 1)), Instant((2024, 1, 1)), 3], - [periods.YEAR, periods.MONTH, Instant((2022, 12, 1)), Instant((2025, 11, 1)), 36], - [periods.YEAR, periods.DAY, Instant((2022, 12, 31)), Instant((2025, 12, 30)), 1096], - [periods.MONTH, periods.MONTH, Instant((2022, 12, 1)), Instant((2023, 2, 1)), 3], - [periods.MONTH, periods.DAY, Instant((2022, 12, 31)), Instant((2023, 3, 30)), 90], - [periods.DAY, periods.DAY, Instant((2022, 12, 31)), Instant((2023, 1, 2)), 3], + [periods.YEAR, periods.YEAR, periods.Instant((2022, 1, 1)), periods.Instant((2024, 1, 1)), 3], + [periods.YEAR, periods.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2025, 11, 1)), 36], + [periods.YEAR, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2025, 12, 30)), 1096], + [periods.MONTH, periods.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2023, 2, 1)), 3], + [periods.MONTH, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2023, 3, 30)), 90], + [periods.DAY, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2023, 1, 2)), 3], ]) def test_subperiods(instant, period_unit, unit, start, cease, count): - period = Period((period_unit, instant, 3)) + period = periods.Period((period_unit, instant, 3)) subperiods = period.get_subperiods(unit) assert len(subperiods) == count - assert subperiods[0] == Period((unit, start, 1)) - assert subperiods[-1] == Period((unit, cease, 1)) + 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.YEAR, "first-of", periods.YEAR, Period(('year', Instant((2022, 1, 1)), 3))], - [periods.YEAR, "first-of", periods.MONTH, Period(('year', Instant((2022, 12, 1)), 3))], - [periods.YEAR, "last-of", periods.YEAR, Period(('year', Instant((2022, 12, 31)), 3))], - [periods.YEAR, "last-of", periods.MONTH, Period(('year', Instant((2022, 12, 31)), 3))], - [periods.YEAR, -3, periods.YEAR, Period(('year', Instant((2019, 12, 31)), 3))], - [periods.YEAR, 1, periods.MONTH, Period(('year', Instant((2023, 1, 31)), 3))], - [periods.YEAR, 3, periods.DAY, Period(('year', Instant((2023, 1, 3)), 3))], - [periods.MONTH, "first-of", periods.YEAR, Period(('month', Instant((2022, 1, 1)), 3))], - [periods.MONTH, "first-of", periods.MONTH, Period(('month', Instant((2022, 12, 1)), 3))], - [periods.MONTH, "last-of", periods.YEAR, Period(('month', Instant((2022, 12, 31)), 3))], - [periods.MONTH, "last-of", periods.MONTH, Period(('month', Instant((2022, 12, 31)), 3))], - [periods.MONTH, -3, periods.YEAR, Period(('month', Instant((2019, 12, 31)), 3))], - [periods.MONTH, 1, periods.MONTH, Period(('month', Instant((2023, 1, 31)), 3))], - [periods.MONTH, 3, periods.DAY, Period(('month', Instant((2023, 1, 3)), 3))], - [periods.DAY, "first-of", periods.YEAR, Period(('day', Instant((2022, 1, 1)), 3))], - [periods.DAY, "first-of", periods.MONTH, Period(('day', Instant((2022, 12, 1)), 3))], - [periods.DAY, "last-of", periods.YEAR, Period(('day', Instant((2022, 12, 31)), 3))], - [periods.DAY, "last-of", periods.MONTH, Period(('day', Instant((2022, 12, 31)), 3))], - [periods.DAY, -3, periods.YEAR, Period(('day', Instant((2019, 12, 31)), 3))], - [periods.DAY, 1, periods.MONTH, Period(('day', Instant((2023, 1, 31)), 3))], - [periods.DAY, 3, periods.DAY, Period(('day', Instant((2023, 1, 3)), 3))], + [periods.YEAR, "first-of", periods.YEAR, periods.Period(('year', periods.Instant((2022, 1, 1)), 3))], + [periods.YEAR, "first-of", periods.MONTH, periods.Period(('year', periods.Instant((2022, 12, 1)), 3))], + [periods.YEAR, "last-of", periods.YEAR, periods.Period(('year', periods.Instant((2022, 12, 31)), 3))], + [periods.YEAR, "last-of", periods.MONTH, periods.Period(('year', periods.Instant((2022, 12, 31)), 3))], + [periods.YEAR, -3, periods.YEAR, periods.Period(('year', periods.Instant((2019, 12, 31)), 3))], + [periods.YEAR, 1, periods.MONTH, periods.Period(('year', periods.Instant((2023, 1, 31)), 3))], + [periods.YEAR, 3, periods.DAY, periods.Period(('year', periods.Instant((2023, 1, 3)), 3))], + [periods.MONTH, "first-of", periods.YEAR, periods.Period(('month', periods.Instant((2022, 1, 1)), 3))], + [periods.MONTH, "first-of", periods.MONTH, periods.Period(('month', periods.Instant((2022, 12, 1)), 3))], + [periods.MONTH, "last-of", periods.YEAR, periods.Period(('month', periods.Instant((2022, 12, 31)), 3))], + [periods.MONTH, "last-of", periods.MONTH, periods.Period(('month', periods.Instant((2022, 12, 31)), 3))], + [periods.MONTH, -3, periods.YEAR, periods.Period(('month', periods.Instant((2019, 12, 31)), 3))], + [periods.MONTH, 1, periods.MONTH, periods.Period(('month', periods.Instant((2023, 1, 31)), 3))], + [periods.MONTH, 3, periods.DAY, periods.Period(('month', periods.Instant((2023, 1, 3)), 3))], + [periods.DAY, "first-of", periods.YEAR, periods.Period(('day', periods.Instant((2022, 1, 1)), 3))], + [periods.DAY, "first-of", periods.MONTH, periods.Period(('day', periods.Instant((2022, 12, 1)), 3))], + [periods.DAY, "last-of", periods.YEAR, periods.Period(('day', periods.Instant((2022, 12, 31)), 3))], + [periods.DAY, "last-of", periods.MONTH, periods.Period(('day', periods.Instant((2022, 12, 31)), 3))], + [periods.DAY, -3, periods.YEAR, periods.Period(('day', periods.Instant((2019, 12, 31)), 3))], + [periods.DAY, 1, periods.MONTH, periods.Period(('day', periods.Instant((2023, 1, 31)), 3))], + [periods.DAY, 3, periods.DAY, periods.Period(('day', periods.Instant((2023, 1, 3)), 3))], ]) def test_offset(instant, period_unit, offset, unit, expected): - period = Period((period_unit, instant, 3)) + period = periods.Period((period_unit, instant, 3)) assert period.offset(offset, unit) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, Instant((2022, 12, 1)), 1, 1], - [periods.MONTH, Instant((2012, 2, 3)), 1, 1], - [periods.MONTH, Instant((2022, 1, 3)), 3, 3], - [periods.MONTH, Instant((2012, 1, 3)), 3, 3], - [periods.YEAR, Instant((2022, 12, 1)), 1, 12], - [periods.YEAR, Instant((2012, 1, 1)), 1, 12], - [periods.YEAR, Instant((2022, 1, 1)), 2, 24], + [periods.MONTH, periods.Instant((2022, 12, 1)), 1, 1], + [periods.MONTH, periods.Instant((2012, 2, 3)), 1, 1], + [periods.MONTH, periods.Instant((2022, 1, 3)), 3, 3], + [periods.MONTH, periods.Instant((2012, 1, 3)), 3, 3], + [periods.YEAR, periods.Instant((2022, 12, 1)), 1, 12], + [periods.YEAR, periods.Instant((2012, 1, 1)), 1, 12], + [periods.YEAR, periods.Instant((2022, 1, 1)), 2, 24], ]) def test_day_size_in_months(date_unit, instant, size, expected): - period = Period((date_unit, instant, size)) + period = periods.Period((date_unit, instant, size)) assert period.size_in_months == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.DAY, Instant((2022, 12, 31)), 1, 1], - [periods.DAY, Instant((2022, 12, 31)), 3, 3], - [periods.MONTH, Instant((2022, 12, 1)), 1, 31], - [periods.MONTH, Instant((2012, 2, 3)), 1, 29], - [periods.MONTH, Instant((2022, 1, 3)), 3, 31 + 28 + 31], - [periods.MONTH, Instant((2012, 1, 3)), 3, 31 + 29 + 31], - [periods.YEAR, Instant((2022, 12, 1)), 1, 365], - [periods.YEAR, Instant((2012, 1, 1)), 1, 366], - [periods.YEAR, Instant((2022, 1, 1)), 2, 730], + [periods.DAY, periods.Instant((2022, 12, 31)), 1, 1], + [periods.DAY, periods.Instant((2022, 12, 31)), 3, 3], + [periods.MONTH, periods.Instant((2022, 12, 1)), 1, 31], + [periods.MONTH, periods.Instant((2012, 2, 3)), 1, 29], + [periods.MONTH, periods.Instant((2022, 1, 3)), 3, 31 + 28 + 31], + [periods.MONTH, periods.Instant((2012, 1, 3)), 3, 31 + 29 + 31], + [periods.YEAR, periods.Instant((2022, 12, 1)), 1, 365], + [periods.YEAR, periods.Instant((2012, 1, 1)), 1, 366], + [periods.YEAR, periods.Instant((2022, 1, 1)), 2, 730], ]) def test_day_size_in_days(date_unit, instant, size, expected): - period = Period((date_unit, instant, size)) + period = periods.Period((date_unit, instant, size)) assert period.size_in_days == expected From f2e80858b48b7a905f525a7a29b0b81f385cdf24 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 01:31:10 +0100 Subject: [PATCH 25/93] Fix style --- openfisca_core/periods/_funcs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index 5299829bd3..fbc4f881bf 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -188,6 +188,7 @@ def parse_simple_period(value: str) -> Optional[types.Period]: else: return Period((_config.YEAR, Instant((date.year, date.month, 1)), 1)) + def period(value: Any) -> types.Period: """Build a new period, aka a triple (unit, start_instant, size). From 35b03dbab9949c4ae92eac91115a9d9213e8fcfb Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 01:36:06 +0100 Subject: [PATCH 26/93] Rename zinstant` to `build_instant` --- CHANGELOG.md | 1 + openfisca_core/parameters/at_instant_like.py | 2 +- openfisca_core/periods/__init__.py | 2 +- openfisca_core/periods/_funcs.py | 16 ++++++++-------- .../taxbenefitsystems/tax_benefit_system.py | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfde1fd9da..4513130103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ #### Breaking changes - Deprecate `periods.intersect`. +- Rename `instant` to `build_instant` #### Technical changes 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/periods/__init__.py b/openfisca_core/periods/__init__.py index 7c229e4e79..6c83ac6492 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -39,7 +39,7 @@ ) from ._funcs import ( # noqa: F401 - instant, + build_instant, instant_date, key_period_size, parse_simple_period, diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index fbc4f881bf..d300d24bb1 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -12,7 +12,7 @@ from .period_ import Period -def instant(value: Any) -> Optional[types.Instant]: +def build_instant(value: Any) -> Optional[types.Instant]: """Build a new instant, aka a triple of integers (year, month, day). Args: @@ -26,22 +26,22 @@ def instant(value: Any) -> Optional[types.Instant]: ValueError: When the arguments were invalid, like "2021-32-13". Examples: - >>> instant(datetime.date(2021, 9, 16)) + >>> build_instant(datetime.date(2021, 9, 16)) Instant((2021, 9, 16)) - >>> instant(Instant((2021, 9, 16))) + >>> build_instant(Instant((2021, 9, 16))) Instant((2021, 9, 16)) - >>> instant(Period(("year", Instant((2021, 9, 16)), 1))) + >>> build_instant(Period(("year", Instant((2021, 9, 16)), 1))) Instant((2021, 9, 16)) - >>> instant("2021") + >>> build_instant("2021") Instant((2021, 1, 1)) - >>> instant(2021) + >>> build_instant(2021) Instant((2021, 1, 1)) - >>> instant((2021, 9)) + >>> build_instant((2021, 9)) Instant((2021, 9, 1)) """ @@ -241,7 +241,7 @@ def period(value: Any) -> types.Period: return Period((_config.DAY, value, 1)) if value == "ETERNITY" or value == _config.ETERNITY: - return Period(("eternity", instant(datetime.date.min), float("inf"))) + return Period(("eternity", build_instant(datetime.date.min), float("inf"))) if isinstance(value, int): return Period((_config.YEAR, Instant((value, 1, 1)), 1)) diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 9a8831269d..4af239ca52 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -407,7 +407,7 @@ 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}." From 746d29ce9ca43b60330cf187f3b1246ba69f16af Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 01:40:44 +0100 Subject: [PATCH 27/93] Rename to --- CHANGELOG.md | 1 + .../data_storage/in_memory_storage.py | 12 +- .../data_storage/on_disk_storage.py | 14 +- openfisca_core/holders/holder.py | 2 +- openfisca_core/model_api.py | 2 +- openfisca_core/parameters/parameter.py | 2 +- openfisca_core/periods/__init__.py | 2 +- openfisca_core/periods/_funcs.py | 214 +++++++++--------- openfisca_core/periods/tests/test_helpers.py | 4 +- openfisca_core/populations/population.py | 2 +- .../scripts/measure_performances.py | 2 +- openfisca_core/simulations/simulation.py | 10 +- .../simulations/simulation_builder.py | 8 +- openfisca_core/variables/variable.py | 2 +- tests/core/test_countries.py | 2 +- tests/core/test_cycles.py | 2 +- tests/core/test_holders.py | 30 +-- tests/core/test_opt_out_cache.py | 2 +- tests/core/test_reforms.py | 34 +-- tests/core/variables/test_annualize.py | 8 +- 20 files changed, 178 insertions(+), 177 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4513130103..29a615634c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Deprecate `periods.intersect`. - Rename `instant` to `build_instant` +- Rename `period` to `build_period` #### Technical changes diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py index bd40460a56..765a6f836c 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.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.ETERNITY) + period = periods.build_period(period) self._arrays[period] = value @@ -35,8 +35,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.period(periods.ETERNITY) - period = periods.period(period) + period = periods.build_period(periods.ETERNITY) + period = periods.build_period(period) self._arrays = { period_item: value diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py index 10d4696b58..402b576e6a 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -28,8 +28,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(periods.ETERNITY) + period = periods.build_period(period) values = self._files.get(period) if values is None: @@ -38,8 +38,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.ETERNITY) + period = periods.build_period(period) filename = str(period) path = os.path.join(self.storage_dir, filename) + '.npy' @@ -55,8 +55,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.period(periods.ETERNITY) - period = periods.period(period) + period = periods.build_period(periods.ETERNITY) + period = periods.build_period(period) if period is not None: self._files = { @@ -76,7 +76,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/holders/holder.py b/openfisca_core/holders/holder.py index ae7e3fbcec..18784b0b91 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -210,7 +210,7 @@ def set_input( """ - period = periods.period(period) + period = periods.build_period(period) if period.unit == periods.ETERNITY and self.variable.definition_period != periods.ETERNITY: error_message = os.linesep.join([ 'Unable to set a value for variable {0} for periods.ETERNITY.', diff --git a/openfisca_core/model_api.py b/openfisca_core/model_api.py index 8ccf5c2763..8e306026d3 100644 --- a/openfisca_core/model_api.py +++ b/openfisca_core/model_api.py @@ -27,7 +27,7 @@ ValuesHistory, ) -from openfisca_core.periods import DAY, MONTH, YEAR, ETERNITY, period # noqa: F401 +from openfisca_core.periods import DAY, MONTH, YEAR, ETERNITY, build_period # noqa: F401 from openfisca_core.populations import ADD, DIVIDE # noqa: F401 from openfisca_core.reforms import Reform # noqa: F401 diff --git a/openfisca_core/parameters/parameter.py b/openfisca_core/parameters/parameter.py index ed2c9482a4..63090bb3a6 100644 --- a/openfisca_core/parameters/parameter.py +++ b/openfisca_core/parameters/parameter.py @@ -120,7 +120,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/periods/__init__.py b/openfisca_core/periods/__init__.py index 6c83ac6492..956daf0a8d 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -40,10 +40,10 @@ from ._funcs import ( # noqa: F401 build_instant, + build_period, instant_date, key_period_size, parse_simple_period, - period, unit_weight, unit_weights, ) diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index d300d24bb1..de00fa687a 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -91,6 +91,113 @@ def build_instant(value: Any) -> Optional[types.Instant]: return Instant(instant) +def build_period(value: Any) -> types.Period: + """Build a new period, aka a triple (unit, start_instant, size). + + Args: + value: A ``period-like`` object. + + Returns: + :obj:`.Period`: A period. + + Raises: + :exc:`ValueError`: When the arguments were invalid, like "2021-32-13". + + Examples: + >>> build_period(Period(("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("eternity") + Period(('eternity', Instant((1, 1, 1)), inf)) + + >>> 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-2") + Period(('month', Instant((2014, 2, 1)), 1)) + + >>> build_period("year:2014-2") + Period(('year', Instant((2014, 2, 1)), 1)) + + >>> build_period("day:2014-2-2") + Period(('day', Instant((2014, 2, 2)), 1)) + + >>> build_period("day:2014-2-2:3") + Period(('day', Instant((2014, 2, 2)), 3)) + + """ + + if isinstance(value, Period): + return value + + if isinstance(value, Instant): + return Period((_config.DAY, value, 1)) + + if value == "ETERNITY" or value == _config.ETERNITY: + return Period(("eternity", build_instant(datetime.date.min), float("inf"))) + + 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 periods 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) + + # Periods 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 are more than 2 ":" in the string, the period is invalid + else: + _raise_error(value) + + # Reject ambiguous periods such as month:2014 + if unit_weight(base_period.unit) > unit_weight(unit): + _raise_error(value) + + return Period((unit, base_period.start, size)) + + def instant_date(instant: Optional[types.Instant]) -> Optional[datetime.date]: """Returns the date representation of an ``Instant``. @@ -189,113 +296,6 @@ def parse_simple_period(value: str) -> Optional[types.Period]: return Period((_config.YEAR, Instant((date.year, date.month, 1)), 1)) -def period(value: Any) -> types.Period: - """Build a new period, aka a triple (unit, start_instant, size). - - Args: - value: A ``period-like`` object. - - Returns: - :obj:`.Period`: A period. - - Raises: - :exc:`ValueError`: When the arguments were invalid, like "2021-32-13". - - Examples: - >>> period(Period(("year", Instant((2021, 1, 1)), 1))) - Period(('year', Instant((2021, 1, 1)), 1)) - - >>> period(Instant((2021, 1, 1))) - Period(('day', Instant((2021, 1, 1)), 1)) - - >>> period("eternity") - Period(('eternity', Instant((1, 1, 1)), inf)) - - >>> period(2021) - Period(('year', Instant((2021, 1, 1)), 1)) - - >>> period("2014") - Period(('year', Instant((2014, 1, 1)), 1)) - - >>> period("year:2014") - Period(('year', Instant((2014, 1, 1)), 1)) - - >>> period("month:2014-2") - Period(('month', Instant((2014, 2, 1)), 1)) - - >>> period("year:2014-2") - Period(('year', Instant((2014, 2, 1)), 1)) - - >>> period("day:2014-2-2") - Period(('day', Instant((2014, 2, 2)), 1)) - - >>> period("day:2014-2-2:3") - Period(('day', Instant((2014, 2, 2)), 3)) - - """ - - if isinstance(value, Period): - return value - - if isinstance(value, Instant): - return Period((_config.DAY, value, 1)) - - if value == "ETERNITY" or value == _config.ETERNITY: - return Period(("eternity", build_instant(datetime.date.min), float("inf"))) - - 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 periods 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) - - # Periods 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 are more than 2 ":" in the string, the period is invalid - else: - _raise_error(value) - - # Reject ambiguous periods such as month:2014 - if unit_weight(base_period.unit) > unit_weight(unit): - _raise_error(value) - - return Period((unit, base_period.start, size)) - - def unit_weights() -> Dict[str, int]: """Assign weights to date units. diff --git a/openfisca_core/periods/tests/test_helpers.py b/openfisca_core/periods/tests/test_helpers.py index 149b0a82fa..333186f3e8 100644 --- a/openfisca_core/periods/tests/test_helpers.py +++ b/openfisca_core/periods/tests/test_helpers.py @@ -91,7 +91,7 @@ def test_instant_date_with_an_invalid_argument(arg, error): ["day:1000-01-01:3", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 3))], ]) def test_period(arg, expected): - assert periods.period(arg) == expected + assert periods.build_period(arg) == expected @pytest.mark.parametrize("arg, error", [ @@ -114,7 +114,7 @@ def test_period(arg, expected): ]) def test_period_with_an_invalid_argument(arg, error): with pytest.raises(error): - periods.period(arg) + periods.build_period(arg) @pytest.mark.parametrize("arg, expected", [ diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index cb243aff70..edf08044f0 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -111,7 +111,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/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index 5c4fced850..5e0e01a218 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -186,7 +186,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/simulations/simulation.py b/openfisca_core/simulations/simulation.py index ef1cbbd869..17cb73ebf7 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -96,7 +96,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,7 +168,7 @@ 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): @@ -197,7 +197,7 @@ 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: @@ -345,7 +345,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,7 +438,7 @@ 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) + 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..71193296b3 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -325,7 +325,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 +368,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 +393,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 +411,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: diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 1e9fce3083..eeb9c33169 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -336,7 +336,7 @@ def get_formula( instant = period.start else: try: - instant = periods.period(period).start + instant = periods.build_period(period).start except ValueError: instant = periods.instant(period) diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index aeb4d762c7..70390e783c 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -5,7 +5,7 @@ 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) diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index 1c4361ded2..dc17aee5de 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 diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index 907aefceb5..928683adcb 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -26,7 +26,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,7 +89,7 @@ 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.ETERNITY), value) assert holder.get_array(None) == value assert holder.get_array(periods.ETERNITY) == value assert holder.get_array('2016-01') == value @@ -98,8 +98,8 @@ def test_permanent_variable_filled(single): 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 +109,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 +119,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 +132,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 +147,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 +159,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 +172,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 +183,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 +196,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..a3a2cf7a31 100644 --- a/tests/core/test_opt_out_cache.py +++ b/tests/core/test_opt_out_cache.py @@ -7,7 +7,7 @@ from openfisca_core.variables import Variable -PERIOD = periods.period("2016-01") +PERIOD = periods.build_period("2016-01") class input(Variable): diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index 8735cee18f..85b54abadf 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -124,23 +124,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 +148,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 +164,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 +180,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 +204,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}}), diff --git a/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index 056fcfead3..8479c09357 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -41,7 +41,7 @@ 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) @@ -55,7 +55,7 @@ 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) @@ -70,8 +70,8 @@ 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) From 03ce49b06a1a8f9dc4c0de7f403babef0ca674b0 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 01:56:37 +0100 Subject: [PATCH 28/93] Move to --- CHANGELOG.md | 1 + openfisca_core/periods/__init__.py | 1 - openfisca_core/periods/_funcs.py | 28 --------------- openfisca_core/periods/instant_.py | 30 +++++++++++++++- openfisca_core/periods/tests/test_helpers.py | 37 ++++---------------- openfisca_core/periods/tests/test_instant.py | 25 +++++++++++++ setup.cfg | 2 +- 7 files changed, 63 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a615634c..14c094800a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Deprecate `periods.intersect`. - Rename `instant` to `build_instant` - Rename `period` to `build_period` +- Move `instant_date` to `Instant.to_date` #### Technical changes diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 956daf0a8d..85fd678d8c 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -41,7 +41,6 @@ from ._funcs import ( # noqa: F401 build_instant, build_period, - instant_date, key_period_size, parse_simple_period, unit_weight, diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index de00fa687a..655ada86ad 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -198,34 +198,6 @@ def build_period(value: Any) -> types.Period: return Period((unit, base_period.start, size)) -def instant_date(instant: Optional[types.Instant]) -> Optional[datetime.date]: - """Returns the date representation of an ``Instant``. - - Args: - instant (:obj:`.Instant`, optional): - An ``instant`` to get the date from. - - Returns: - None: When ``instant`` is None. - datetime.date: Otherwise. - - Examples: - >>> instant_date(Instant((2021, 1, 1))) - datetime.date(2021, 1, 1) - - """ - - 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 key_period_size(period: types.Period) -> str: """Define a key in order to sort periods by length. diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 0a1f677006..aa8ad29d76 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Union +from typing import Optional, Union import calendar import datetime @@ -144,6 +144,34 @@ def date(self) -> datetime.date: return instant_date + @staticmethod + def to_date(instant: Optional[types.Instant]) -> Optional[datetime.date]: + """Returns the date representation of an ``Instant``. + + Args: + instant (:obj:`.Instant`, optional): + An ``instant`` to get the date from. + + Returns: + None: When ``instant`` is None. + datetime.date: Otherwise. + + Examples: + >>> Instant.to_date(Instant((2021, 1, 1))) + datetime.date(2021, 1, 1) + + """ + + if instant is None: + return None + + date = _config.date_by_instant_cache.get(instant) + + if date is None: + _config.date_by_instant_cache[instant] = date = datetime.date(*instant) + + return date + def offset(self, offset: Union[str, int], unit: str) -> types.Instant: """Increments/decrements the given instant with offset units. diff --git a/openfisca_core/periods/tests/test_helpers.py b/openfisca_core/periods/tests/test_helpers.py index 333186f3e8..1951c7eeb6 100644 --- a/openfisca_core/periods/tests/test_helpers.py +++ b/openfisca_core/periods/tests/test_helpers.py @@ -15,8 +15,8 @@ ["1000-01", periods.Instant((1000, 1, 1))], ["1000-01-01", periods.Instant((1000, 1, 1))], ]) -def test_instant(arg, expected): - assert periods.instant(arg) == expected +def test_build_instant(arg, expected): + assert periods.build_instant(arg) == expected @pytest.mark.parametrize("arg, error", [ @@ -36,32 +36,9 @@ def test_instant(arg, expected): ["year:1000-01-01:1", ValueError], ["year:1000-01-01:3", ValueError], ]) -def test_instant_with_an_invalid_argument(arg, error): +def test_build_instant_with_an_invalid_argument(arg, error): with pytest.raises(error): - periods.instant(arg) - - -@pytest.mark.parametrize("arg, expected", [ - [None, None], - [periods.Instant((1, 1, 1)), datetime.date(1, 1, 1)], - [periods.Instant((4, 2, 29)), datetime.date(4, 2, 29)], - [(1, 1, 1), datetime.date(1, 1, 1)], - ]) -def test_instant_date(arg, expected): - assert periods.instant_date(arg) == expected - - -@pytest.mark.parametrize("arg, error", [ - [periods.Instant((-1, 1, 1)), ValueError], - [periods.Instant((1, -1, 1)), ValueError], - [periods.Instant((1, 1, -1)), ValueError], - [periods.Instant((1, 13, 1)), ValueError], - [periods.Instant((1, 1, 32)), ValueError], - [periods.Instant((1, 2, 29)), ValueError], - ]) -def test_instant_date_with_an_invalid_argument(arg, error): - with pytest.raises(error): - periods.instant_date(arg) + periods.build_instant(arg) @pytest.mark.parametrize("arg, expected", [ @@ -90,7 +67,7 @@ def test_instant_date_with_an_invalid_argument(arg, error): ["day:1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], ["day:1000-01-01:3", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 3))], ]) -def test_period(arg, expected): +def test_build_period(arg, expected): assert periods.build_period(arg) == expected @@ -112,7 +89,7 @@ def test_period(arg, expected): ["day:1000-01", ValueError], ["day:1000-01:1", ValueError], ]) -def test_period_with_an_invalid_argument(arg, error): +def test_build_period_with_an_invalid_argument(arg, error): with pytest.raises(error): periods.build_period(arg) @@ -128,7 +105,7 @@ def test_period_with_an_invalid_argument(arg, error): ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], ["1000-01-99", None], ]) -def testparse_simple_period(arg, expected): +def test_parse_simple_period(arg, expected): assert periods.parse_simple_period(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index a0c098cc32..cc17e5bf8b 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -1,3 +1,5 @@ +import datetime + import pytest from openfisca_core import periods @@ -8,6 +10,29 @@ def instant(): return periods.Instant((2020, 2, 29)) +@pytest.mark.parametrize("arg, expected", [ + [None, None], + [periods.Instant((1, 1, 1)), datetime.date(1, 1, 1)], + [periods.Instant((4, 2, 29)), datetime.date(4, 2, 29)], + [(1, 1, 1), datetime.date(1, 1, 1)], + ]) +def test_to_date(arg, expected): + assert periods.Instant.to_date(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + [periods.Instant((-1, 1, 1)), ValueError], + [periods.Instant((1, -1, 1)), ValueError], + [periods.Instant((1, 1, -1)), ValueError], + [periods.Instant((1, 13, 1)), ValueError], + [periods.Instant((1, 1, 32)), ValueError], + [periods.Instant((1, 2, 29)), ValueError], + ]) +def test_to_date_with_an_invalid_argument(arg, error): + with pytest.raises(error): + periods.Instant.to_date(arg) + + @pytest.mark.parametrize("offset, unit, expected", [ ["first-of", periods.YEAR, periods.Instant((2020, 1, 1))], ["first-of", periods.MONTH, periods.Instant((2020, 2, 1))], diff --git a/setup.cfg b/setup.cfg index 2144a48610..d66ba84ea3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,7 +41,7 @@ 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 From 4afec7a0de8eaefe77465001d3244c1c0325e41a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 02:24:16 +0100 Subject: [PATCH 29/93] Do not expose Instant cache --- openfisca_core/periods/__init__.py | 3 -- openfisca_core/periods/_config.py | 4 -- openfisca_core/periods/instant_.py | 81 ++++++++++++++++-------------- openfisca_core/periods/period_.py | 2 +- 4 files changed, 43 insertions(+), 47 deletions(-) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 85fd678d8c..a4402787dc 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -33,9 +33,6 @@ YEAR, ETERNITY, INSTANT_PATTERN, - date_by_instant_cache, - str_by_instant_cache, - year_or_month_or_day_re, ) from ._funcs import ( # noqa: F401 diff --git a/openfisca_core/periods/_config.py b/openfisca_core/periods/_config.py index 657831d527..92c8497767 100644 --- a/openfisca_core/periods/_config.py +++ b/openfisca_core/periods/_config.py @@ -12,7 +12,3 @@ # Matches "2015", "2015-01", "2015-01-01" # Does not match "2015-13", "2015-12-32" INSTANT_PATTERN: 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: Dict = {} -str_by_instant_cache: Dict = {} -year_or_month_or_day_re: Pattern = 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/instant_.py b/openfisca_core/periods/instant_.py index aa8ad29d76..be547f2c1c 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,13 +1,13 @@ from __future__ import annotations -from typing import Optional, Union +from typing import Any, Dict, Optional, Union import calendar import datetime from openfisca_core import types -from . import _config +from . import _config, _funcs class Instant(tuple): @@ -64,16 +64,47 @@ class Instant(tuple): """ + #: A cache with the ``datetime.date`` representation of the ``Instant``. + dates: Dict[Instant, datetime.date] = {} + + #: A cache with the ``str`` representation of the ``Instant``. + strings: Dict[Instant, str] = {} + def __repr__(self) -> str: - return f"{self.__class__.__name__}({super(Instant, self).__repr__()})" + return f"{Instant.__name__}({super(Instant, self).__repr__()})" def __str__(self) -> str: - instant_str = _config.str_by_instant_cache.get(self) + string = Instant.strings.get(self) + + if string is None: + Instant.strings[self] = self.date.isoformat() - if instant_str is None: - _config.str_by_instant_cache[self] = instant_str = self.date.isoformat() + return Instant.strings[self] + + @staticmethod + def to_date(value: Any) -> Optional[datetime.date]: + """Returns the date representation of an ``Instant``. + + Args: + value (:any:): + An ``instant-like`` object to get the date from. + + Returns: + None: When ``value`` is None. + datetime.date: Otherwise. + + Examples: + >>> Instant.to_date(Instant((2021, 1, 1))) + datetime.date(2021, 1, 1) + + """ + + instant = _funcs.build_instant(value) + + if instant is None: + return None - return instant_str + return instant.date @property def year(self) -> int: @@ -137,40 +168,12 @@ def date(self) -> datetime.date: """ - 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 - - @staticmethod - def to_date(instant: Optional[types.Instant]) -> Optional[datetime.date]: - """Returns the date representation of an ``Instant``. - - Args: - instant (:obj:`.Instant`, optional): - An ``instant`` to get the date from. - - Returns: - None: When ``instant`` is None. - datetime.date: Otherwise. - - Examples: - >>> Instant.to_date(Instant((2021, 1, 1))) - datetime.date(2021, 1, 1) - - """ - - if instant is None: - return None - - date = _config.date_by_instant_cache.get(instant) + date = Instant.dates.get(self) if date is None: - _config.date_by_instant_cache[instant] = date = datetime.date(*instant) + Instant.dates[self] = datetime.date(*self) - return date + return Instant.dates[self] def offset(self, offset: Union[str, int], unit: str) -> types.Instant: """Increments/decrements the given instant with offset units. @@ -277,4 +280,4 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: if day > month_last_day: day = month_last_day - return self.__class__((year, month, day)) + return Instant((year, month, day)) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index a858f0abe8..4f287006d1 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -75,7 +75,7 @@ class Period(tuple): """ def __repr__(self) -> str: - return f"{self.__class__.__name__}({super(Period, self).__repr__()})" + return f"{Period.__name__}({super(Period, self).__repr__()})" def __str__(self) -> str: """Transform period to a string. From 4a3b32d6c5b09adacced3bf45a7b860ef779d8da Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 04:54:38 +0100 Subject: [PATCH 30/93] Fix documentation --- openfisca_core/periods/_config.py | 2 +- openfisca_core/periods/_errors.py | 0 openfisca_core/periods/_funcs.py | 64 ++++----- openfisca_core/periods/_units.py | 4 + openfisca_core/periods/instant_.py | 46 +++--- openfisca_core/periods/period_.py | 221 +++++++++++++++++++---------- openfisca_core/periods/py.typed | 0 openfisca_core/types/_domain.py | 5 + 8 files changed, 209 insertions(+), 133 deletions(-) create mode 100644 openfisca_core/periods/_errors.py create mode 100644 openfisca_core/periods/_units.py create mode 100644 openfisca_core/periods/py.typed diff --git a/openfisca_core/periods/_config.py b/openfisca_core/periods/_config.py index 92c8497767..68dc243e10 100644 --- a/openfisca_core/periods/_config.py +++ b/openfisca_core/periods/_config.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Dict, Pattern +from typing import Pattern import re diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index 655ada86ad..4002c1e10e 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -7,9 +7,7 @@ from openfisca_core import types -from . import _config -from .instant_ import Instant -from .period_ import Period +from .. import periods def build_instant(value: Any) -> Optional[types.Instant]: @@ -29,10 +27,10 @@ def build_instant(value: Any) -> Optional[types.Instant]: >>> build_instant(datetime.date(2021, 9, 16)) Instant((2021, 9, 16)) - >>> build_instant(Instant((2021, 9, 16))) + >>> build_instant(periods.Instant((2021, 9, 16))) Instant((2021, 9, 16)) - >>> build_instant(Period(("year", Instant((2021, 9, 16)), 1))) + >>> build_instant(periods.Period(("year", periods.Instant((2021, 9, 16)), 1))) Instant((2021, 9, 16)) >>> build_instant("2021") @@ -49,23 +47,23 @@ def build_instant(value: Any) -> Optional[types.Instant]: if value is None: return None - if isinstance(value, Instant): + if isinstance(value, periods.Instant): return value if isinstance(value, str): - if not _config.INSTANT_PATTERN.match(value): + if not periods.INSTANT_PATTERN.match(value): raise ValueError( f"'{value}' is not a valid instant. Instants are described" "using the 'YYYY-MM-DD' format, for instance '2015-06-15'." ) - instant = Instant( + instant = periods.Instant( int(fragment) for fragment in value.split('-', 2)[:3] ) elif isinstance(value, datetime.date): - instant = Instant((value.year, value.month, value.day)) + instant = periods.Instant((value.year, value.month, value.day)) elif isinstance(value, int): instant = (value,) @@ -74,7 +72,7 @@ def build_instant(value: Any) -> Optional[types.Instant]: assert 1 <= len(value) <= 3 instant = tuple(value) - elif isinstance(value, Period): + elif isinstance(value, periods.Period): instant = value.start else: @@ -83,12 +81,12 @@ def build_instant(value: Any) -> Optional[types.Instant]: instant = value if len(instant) == 1: - return Instant((instant[0], 1, 1)) + return periods.Instant((instant[0], 1, 1)) if len(instant) == 2: - return Instant((instant[0], instant[1], 1)) + return periods.Instant((instant[0], instant[1], 1)) - return Instant(instant) + return periods.Instant(instant) def build_period(value: Any) -> types.Period: @@ -104,10 +102,10 @@ def build_period(value: Any) -> types.Period: :exc:`ValueError`: When the arguments were invalid, like "2021-32-13". Examples: - >>> build_period(Period(("year", Instant((2021, 1, 1)), 1))) + >>> build_period(periods.Period(("year", periods.Instant((2021, 1, 1)), 1))) Period(('year', Instant((2021, 1, 1)), 1)) - >>> build_period(Instant((2021, 1, 1))) + >>> build_period(periods.Instant((2021, 1, 1))) Period(('day', Instant((2021, 1, 1)), 1)) >>> build_period("eternity") @@ -136,17 +134,17 @@ def build_period(value: Any) -> types.Period: """ - if isinstance(value, Period): + if isinstance(value, periods.Period): return value - if isinstance(value, Instant): - return Period((_config.DAY, value, 1)) + if isinstance(value, periods.Instant): + return periods.Period((periods.DAY, value, 1)) - if value == "ETERNITY" or value == _config.ETERNITY: - return Period(("eternity", build_instant(datetime.date.min), float("inf"))) + if value == "ETERNITY" or value == periods.ETERNITY: + return periods.Period(("eternity", build_instant(datetime.date.min), float("inf"))) if isinstance(value, int): - return Period((_config.YEAR, Instant((value, 1, 1)), 1)) + return periods.Period((periods.YEAR, periods.Instant((value, 1, 1)), 1)) if not isinstance(value, str): _raise_error(value) @@ -166,7 +164,7 @@ def build_period(value: Any) -> types.Period: # Left-most component must be a valid unit unit = components[0] - if unit not in (_config.DAY, _config.MONTH, _config.YEAR): + if unit not in (periods.DAY, periods.MONTH, periods.YEAR): _raise_error(value) # Middle component must be a valid iso period @@ -195,7 +193,7 @@ def build_period(value: Any) -> types.Period: if unit_weight(base_period.unit) > unit_weight(unit): _raise_error(value) - return Period((unit, base_period.start, size)) + return periods.Period((unit, base_period.start, size)) def key_period_size(period: types.Period) -> str: @@ -210,13 +208,13 @@ def key_period_size(period: types.Period) -> str: :obj:`str`: A string. Examples: - >>> instant = Instant((2021, 9, 14)) + >>> instant = periods.Instant((2021, 9, 14)) - >>> period = Period(("day", instant, 1)) + >>> period = periods.Period(("day", instant, 1)) >>> key_period_size(period) '100_1' - >>> period = Period(("year", instant, 3)) + >>> period = periods.Period(("year", instant, 3)) >>> key_period_size(period) '300_3' @@ -259,13 +257,13 @@ def parse_simple_period(value: str) -> Optional[types.Period]: return None else: - return Period((_config.DAY, Instant((date.year, date.month, date.day)), 1)) + return periods.Period((periods.DAY, periods.Instant((date.year, date.month, date.day)), 1)) else: - return Period((_config.MONTH, Instant((date.year, date.month, 1)), 1)) + return periods.Period((periods.MONTH, periods.Instant((date.year, date.month, 1)), 1)) else: - return Period((_config.YEAR, Instant((date.year, date.month, 1)), 1)) + return periods.Period((periods.YEAR, periods.Instant((date.year, date.month, 1)), 1)) def unit_weights() -> Dict[str, int]: @@ -278,10 +276,10 @@ def unit_weights() -> Dict[str, int]: """ return { - _config.DAY: 100, - _config.MONTH: 200, - _config.YEAR: 300, - _config.ETERNITY: 400, + periods.DAY: 100, + periods.MONTH: 200, + periods.YEAR: 300, + periods.ETERNITY: 400, } diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_units.py new file mode 100644 index 0000000000..5b139ab0d0 --- /dev/null +++ b/openfisca_core/periods/_units.py @@ -0,0 +1,4 @@ +DAY = "day" +MONTH = "month" +YEAR = "year" +ETERNITY = "eternity" diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index be547f2c1c..f2f5cba021 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -7,7 +7,7 @@ from openfisca_core import types -from . import _config, _funcs +from .. import periods class Instant(tuple): @@ -65,28 +65,30 @@ class Instant(tuple): """ #: A cache with the ``datetime.date`` representation of the ``Instant``. - dates: Dict[Instant, datetime.date] = {} + _dates: Dict[Instant, datetime.date] = {} #: A cache with the ``str`` representation of the ``Instant``. - strings: Dict[Instant, str] = {} + _strings: Dict[Instant, str] = {} def __repr__(self) -> str: return f"{Instant.__name__}({super(Instant, self).__repr__()})" def __str__(self) -> str: - string = Instant.strings.get(self) + string = Instant._strings.get(self) - if string is None: - Instant.strings[self] = self.date.isoformat() + if string is not None: + return string - return Instant.strings[self] + Instant._strings = {self: self.date.isoformat(), **Instant._strings} + + return str(self) @staticmethod def to_date(value: Any) -> Optional[datetime.date]: """Returns the date representation of an ``Instant``. Args: - value (:any:): + value (Any): An ``instant-like`` object to get the date from. Returns: @@ -99,7 +101,7 @@ def to_date(value: Any) -> Optional[datetime.date]: """ - instant = _funcs.build_instant(value) + instant = periods.build_instant(value) if instant is None: return None @@ -168,12 +170,14 @@ def date(self) -> datetime.date: """ - date = Instant.dates.get(self) + date = Instant._dates.get(self) + + if date is not None: + return date - if date is None: - Instant.dates[self] = datetime.date(*self) + Instant._dates = {self: datetime.date(*self), **Instant._dates} - return Instant.dates[self] + return self.date def offset(self, offset: Union[str, int], unit: str) -> types.Instant: """Increments/decrements the given instant with offset units. @@ -207,28 +211,28 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: year, month, day = self - assert unit in (_config.DAY, _config.MONTH, _config.YEAR), 'Invalid unit: {} of type {}'.format(unit, type(unit)) + assert unit in (periods.DAY, periods.MONTH, periods.YEAR), 'Invalid unit: {} of type {}'.format(unit, type(unit)) if offset == 'first-of': - if unit == _config.MONTH: + if unit == periods.MONTH: day = 1 - elif unit == _config.YEAR: + elif unit == periods.YEAR: month = 1 day = 1 elif offset == 'last-of': - if unit == _config.MONTH: + if unit == periods.MONTH: day = calendar.monthrange(year, month)[1] - elif unit == _config.YEAR: + elif unit == periods.YEAR: month = 12 day = 31 else: assert isinstance(offset, int), 'Invalid offset: {} of type {}'.format(offset, type(offset)) - if unit == _config.DAY: + if unit == periods.DAY: day += offset if offset < 0: @@ -254,7 +258,7 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: day -= month_last_day month_last_day = calendar.monthrange(year, month)[1] - elif unit == _config.MONTH: + elif unit == periods.MONTH: month += offset if offset < 0: @@ -271,7 +275,7 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: if day > month_last_day: day = month_last_day - elif unit == _config.YEAR: + elif unit == periods.YEAR: year += offset # Handle february month of leap year. diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 4f287006d1..11e2f853ea 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -7,8 +7,7 @@ from openfisca_core import types -from . import _config, _funcs -from .instant_ import Instant +from .. import periods class Period(tuple): @@ -29,8 +28,8 @@ class Period(tuple): The ``unit``, ``start``, and ``size``, accordingly. Examples: - >>> instant = Instant((2021, 9, 1)) - >>> period = Period((_config.YEAR, instant, 3)) + >>> instant = periods.Instant((2021, 9, 1)) + >>> period = periods.Period((periods.YEAR, instant, 3)) ``Periods`` are represented as a ``tuple`` containing the ``unit``, an ``Instant`` and the ``size``: @@ -62,10 +61,10 @@ class Period(tuple): >>> len(period) 3 - >>> period == Period(("year", instant, 3)) + >>> period == periods.Period(("year", instant, 3)) True - >>> period > Period(("year", instant, 3)) + >>> period > periods.Period(("year", instant, 3)) False >>> unit, (year, month, day), size = period @@ -80,59 +79,62 @@ def __repr__(self) -> str: def __str__(self) -> str: """Transform period to a string. + Returns: + str: A string representation of the period. + Examples: - >>> str(Period(("year", Instant((2021, 1, 1)), 1))) + >>> str(Period(("year", periods.Instant((2021, 1, 1)), 1))) '2021' - >>> str(Period(("year", Instant((2021, 2, 1)), 1))) + >>> str(Period(("year", periods.Instant((2021, 2, 1)), 1))) 'year:2021-02' - >>> str(Period(("month", Instant((2021, 2, 1)), 1))) + >>> str(Period(("month", periods.Instant((2021, 2, 1)), 1))) '2021-02' - >>> str(Period(("year", Instant((2021, 1, 1)), 2))) + >>> str(Period(("year", periods.Instant((2021, 1, 1)), 2))) 'year:2021:2' - >>> str(Period(("month", Instant((2021, 1, 1)), 2))) + >>> str(Period(("month", periods.Instant((2021, 1, 1)), 2))) 'month:2021-01:2' - >>> str(Period(("month", Instant((2021, 1, 1)), 12))) + >>> str(Period(("month", periods.Instant((2021, 1, 1)), 12))) '2021' - >>> str(Period(("year", Instant((2021, 3, 1)), 2))) + >>> str(Period(("year", periods.Instant((2021, 3, 1)), 2))) 'year:2021-03:2' - >>> str(Period(("month", Instant((2021, 3, 1)), 2))) + >>> str(Period(("month", periods.Instant((2021, 3, 1)), 2))) 'month:2021-03:2' - >>> str(Period(("month", Instant((2021, 3, 1)), 12))) + >>> str(Period(("month", periods.Instant((2021, 3, 1)), 12))) 'year:2021-03' """ unit, start_instant, size = self - if unit == _config.ETERNITY: + if unit == periods.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 (unit == periods.MONTH and size == 12 or unit == periods.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) + return '{}:{}-{:02d}'.format(periods.YEAR, year, month) # simple month - if unit == _config.MONTH and size == 1: + if unit == periods.MONTH and size == 1: return '{}-{:02d}'.format(year, month) # several civil years - if unit == _config.YEAR and month == 1: + if unit == periods.YEAR and month == 1: return '{}:{}:{}'.format(unit, year, size) - if unit == _config.DAY: + if unit == periods.DAY: if size == 1: return '{}-{:02d}-{:02d}'.format(year, month, day) else: @@ -148,13 +150,16 @@ def date(self) -> datetime.date: Returns: A datetime.date. + Raises: + ValueError: If the period's size is greater than 1. + Examples: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((_config.YEAR, instant, 1)) + >>> instant = periods.Instant((2021, 10, 1)) + >>> period = periods.Period((periods.YEAR, instant, 1)) >>> period.date datetime.date(2021, 10, 1) - >>> period = Period((_config.YEAR, instant, 3)) + >>> period = periods.Period((periods.YEAR, instant, 3)) >>> period.date Traceback (most recent call last): ValueError: "date" is undefined for a period of size > 1: year:2021-10:3. @@ -174,8 +179,8 @@ def unit(self) -> str: An int. Example: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((_config.YEAR, instant, 3)) + >>> instant = periods.Instant((2021, 10, 1)) + >>> period = periods.Period((periods.YEAR, instant, 3)) >>> period.unit 'year' @@ -191,12 +196,12 @@ def days(self) -> int: An int. Examples: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((_config.YEAR, instant, 3)) + >>> instant = periods.Instant((2021, 10, 1)) + >>> period = periods.Period((periods.YEAR, instant, 3)) >>> period.size_in_days 1096 - >>> period = Period((_config.MONTH, instant, 3)) + >>> period = periods.Period((periods.MONTH, instant, 3)) >>> period.size_in_days 92 @@ -212,8 +217,8 @@ def size(self) -> int: An int. Example: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((_config.YEAR, instant, 3)) + >>> instant = periods.Instant((2021, 10, 1)) + >>> period = periods.Period((periods.YEAR, instant, 3)) >>> period.size 3 @@ -228,23 +233,26 @@ def size_in_months(self) -> int: 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((_config.YEAR, instant, 3)) + >>> instant = periods.Instant((2021, 10, 1)) + >>> period = periods.Period((periods.YEAR, instant, 3)) >>> period.size_in_months 36 - >>> period = Period((_config.DAY, instant, 3)) + >>> period = periods.Period((periods.DAY, instant, 3)) >>> period.size_in_months Traceback (most recent call last): ValueError: Cannot calculate number of months in day. """ - if (self[0] == _config.MONTH): + if (self[0] == periods.MONTH): return self[2] - if(self[0] == _config.YEAR): + if(self[0] == periods.YEAR): return self[2] * 12 raise ValueError(f"Cannot calculate number of months in {self[0]}.") @@ -253,13 +261,19 @@ def size_in_months(self) -> int: 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((_config.YEAR, instant, 3)) + >>> instant = periods.Instant((2019, 10, 1)) + >>> period = periods.Period((periods.YEAR, instant, 3)) >>> period.size_in_days 1096 - >>> period = Period((_config.MONTH, instant, 3)) + >>> period = periods.Period((periods.MONTH, instant, 3)) >>> period.size_in_days 92 @@ -267,11 +281,11 @@ def size_in_days(self) -> int: unit, instant, length = self - if unit == _config.DAY: + if unit == periods.DAY: return length - if unit in [_config.MONTH, _config.YEAR]: - last_day = self.start.offset(length, unit).offset(-1, _config.DAY) + if unit in [periods.MONTH, periods.YEAR]: + last_day = self.start.offset(length, unit).offset(-1, periods.DAY) return (last_day.date - self.start.date).days + 1 raise ValueError(f"Cannot calculate number of days in {unit}") @@ -284,8 +298,8 @@ def start(self) -> types.Instant: An Instant. Example: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((_config.YEAR, instant, 3)) + >>> instant = periods.Instant((2021, 10, 1)) + >>> period = periods.Period((periods.YEAR, instant, 3)) >>> period.start Instant((2021, 10, 1)) @@ -301,13 +315,13 @@ def stop(self) -> types.Instant: An Instant. Examples: - >>> Period(("year", Instant((2012, 2, 29)), 1)).stop + >>> periods.Period(("year", periods.Instant((2012, 2, 29)), 1)).stop Instant((2013, 2, 28)) - >>> Period(("month", Instant((2012, 2, 29)), 1)).stop + >>> periods.Period(("month", periods.Instant((2012, 2, 29)), 1)).stop Instant((2012, 3, 28)) - >>> Period(("day", Instant((2012, 2, 29)), 1)).stop + >>> periods.Period(("day", periods.Instant((2012, 2, 29)), 1)).stop Instant((2012, 2, 29)) """ @@ -315,8 +329,8 @@ def stop(self) -> types.Instant: unit, start_instant, size = self year, month, day = start_instant - if unit == _config.ETERNITY: - return Instant((float("inf"), float("inf"), float("inf"))) + if unit == periods.ETERNITY: + return periods.Instant((float("inf"), float("inf"), float("inf"))) if unit == 'day': if size > 1: @@ -355,66 +369,117 @@ def stop(self) -> types.Instant: month = 1 day -= month_last_day - return Instant((year, month, day)) + return periods.Instant((year, month, day)) @property def last_month(self) -> types.Period: + """Last month of the ``Period``. + + Returns: + A Period. + + """ + return self.first_month.offset(-1) @property def last_3_months(self) -> types.Period: + """Last 3 months of the ``Period``. + + Returns: + A Period. + + """ + start: types.Instant = self.first_month.start - return self.__class__((_config.MONTH, start, 3)).offset(-3) + return self.__class__((periods.MONTH, start, 3)).offset(-3) @property def last_year(self) -> types.Period: - start: types.Instant = self.start.offset("first-of", _config.YEAR) - return self.__class__((_config.YEAR, start, 1)).offset(-1) + """Last year of the ``Period``.""" + start: types.Instant = self.start.offset("first-of", periods.YEAR) + return self.__class__((periods.YEAR, start, 1)).offset(-1) @property def n_2(self) -> types.Period: - start: types.Instant = self.start.offset("first-of", _config.YEAR) - return self.__class__((_config.YEAR, start, 1)).offset(-2) + """Last 2 years of the ``Period``. + + Returns: + A Period. + + """ + + start: types.Instant = self.start.offset("first-of", periods.YEAR) + return self.__class__((periods.YEAR, start, 1)).offset(-2) @property def this_year(self) -> types.Period: - start: types.Instant = self.start.offset("first-of", _config.YEAR) - return self.__class__((_config.YEAR, start, 1)) + """A new year ``Period`` starting at the beginning of the year. + + Returns: + A Period. + + """ + + start: types.Instant = self.start.offset("first-of", periods.YEAR) + return self.__class__((periods.YEAR, start, 1)) @property def first_month(self) -> types.Period: - start: types.Instant = self.start.offset("first-of", _config.MONTH) - return self.__class__((_config.MONTH, start, 1)) + """A new month ``Period`` starting at the first of the month. + + Returns: + A Period. + + """ + + start: types.Instant = self.start.offset("first-of", periods.MONTH) + return self.__class__((periods.MONTH, start, 1)) @property def first_day(self) -> types.Period: - return self.__class__((_config.DAY, self.start, 1)) + """A new day ``Period``. + + Returns: + A Period. + + """ + return self.__class__((periods.DAY, self.start, 1)) def get_subperiods(self, unit: str) -> Sequence[types.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: + ValueError: If the period's unit is smaller than the given unit. + Examples: - >>> period = Period((_config.YEAR, Instant((2021, 1, 1)), 1)) - >>> period.get_subperiods(_config.MONTH) + >>> period = periods.Period((periods.YEAR, periods.Instant((2021, 1, 1)), 1)) + >>> period.get_subperiods(periods.MONTH) [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] - >>> period = Period((_config.YEAR, Instant((2021, 1, 1)), 2)) - >>> period.get_subperiods(_config.YEAR) + >>> period = periods.Period((periods.YEAR, periods.Instant((2021, 1, 1)), 2)) + >>> period.get_subperiods(periods.YEAR) [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] """ - if _funcs.unit_weight(self.unit) < _funcs.unit_weight(unit): + if periods.unit_weight(self.unit) < periods.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 == periods.YEAR: + return [self.this_year.offset(i, periods.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 == periods.MONTH: + return [self.first_month.offset(i, periods.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)] + if unit == periods.DAY: + return [self.first_day.offset(i, periods.DAY) for i in range(self.size_in_days)] def offset( self, @@ -431,16 +496,16 @@ def offset( Period: A new one. Examples: - >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("first-of", "month") + >>> periods.Period(("day", periods.Instant((2014, 2, 3)), 1)).offset("first-of", "month") Period(('day', Instant((2014, 2, 1)), 1)) - >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of", "month") + >>> periods.Period(("month", periods.Instant((2014, 2, 3)), 4)).offset("last-of", "month") Period(('month', Instant((2014, 2, 28)), 4)) - >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(-3) + >>> periods.Period(("day", periods.Instant((2021, 1, 1)), 365)).offset(-3) Period(('day', Instant((2020, 12, 29)), 365)) - >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "year") + >>> periods.Period(("day", periods.Instant((2021, 1, 1)), 365)).offset(1, "year") Period(('day', Instant((2022, 1, 1)), 365)) """ @@ -453,12 +518,12 @@ def contains(self, other: types.Period) -> bool: Args: other (:obj:`.Period`): The other ``Period``. - Returns + Returns: True if ``other`` is contained, otherwise False. Example: - >>> period = Period((_config.YEAR, Instant((2021, 1, 1)), 1)) - >>> sub_period = Period((_config.MONTH, Instant((2021, 1, 1)), 3)) + >>> period = periods.Period((periods.YEAR, periods.Instant((2021, 1, 1)), 1)) + >>> sub_period = periods.Period((periods.MONTH, periods.Instant((2021, 1, 1)), 3)) >>> period.contains(sub_period) True 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/types/_domain.py b/openfisca_core/types/_domain.py index 3c64682d08..951e752b1c 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types/_domain.py @@ -58,6 +58,11 @@ def get_memory_usage(self) -> Any: class Instant(Protocol): """Instant protocol.""" + @property + @abc.abstractmethod + def date(self) -> Any: + """Abstract method.""" + @abc.abstractmethod def offset(self, offset: Any, unit: Any) -> Any: """Abstract method.""" From 3c667e3c85d986fc0c2df632b0606220d967ce62 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 05:15:31 +0100 Subject: [PATCH 31/93] Fix circular dependencies --- CHANGELOG.md | 10 +- openfisca_core/periods/__init__.py | 18 ++- openfisca_core/periods/_config.py | 5 - openfisca_core/periods/_errors.py | 64 ++++++++ openfisca_core/periods/_funcs.py | 144 ++++++----------- openfisca_core/periods/_units.py | 7 + openfisca_core/periods/instant_.py | 44 +++--- openfisca_core/periods/period_.py | 153 ++++++++++--------- openfisca_core/periods/tests/test_helpers.py | 8 + openfisca_core/periods/tests/test_instant.py | 7 +- openfisca_core/periods/tests/test_period.py | 16 ++ openfisca_core/simulations/simulation.py | 2 +- openfisca_core/types/_domain.py | 1 + setup.cfg | 2 +- tests/web_api/test_calculate.py | 6 +- 15 files changed, 277 insertions(+), 210 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c094800a..afd5ba81f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,17 @@ #### Breaking changes - Deprecate `periods.intersect`. -- Rename `instant` to `build_instant` -- Rename `period` to `build_period` -- Move `instant_date` to `Instant.to_date` +- Deprecate `periods.unit_weight`. +- Deprecate `periods.unit_weights`. +- Rename `instant` to `build_instant`. +- Rename `period` to `build_period`. +- Move `instant_date` to `Instant.to_date`. #### Technical changes +- Add typing to `openfisca_core.periods`. - Fix `openfisca_core.periods` doctests. +- Document `openfisca_core.periods`. # 38.0.0 [#989](https://github.com/openfisca/openfisca-core/pull/989) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index a4402787dc..3ba24cf9db 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -28,20 +28,26 @@ """ from ._config import ( # noqa: F401 - DAY, - MONTH, - YEAR, - ETERNITY, INSTANT_PATTERN, ) +from ._errors import ( # noqa: F401 + InstantTypeError, + ) + from ._funcs import ( # noqa: F401 build_instant, build_period, key_period_size, parse_simple_period, - unit_weight, - unit_weights, + ) + +from ._units import ( # noqa: F401 + DAY, + ETERNITY, + MONTH, + YEAR, + UNIT_WEIGHTS, ) from .instant_ import Instant # noqa: F401 diff --git a/openfisca_core/periods/_config.py b/openfisca_core/periods/_config.py index 68dc243e10..485f5fd44a 100644 --- a/openfisca_core/periods/_config.py +++ b/openfisca_core/periods/_config.py @@ -4,11 +4,6 @@ import re -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: Pattern = re.compile(r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$") diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index e69de29bb2..b412fffda0 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from typing import Any + +from openfisca_core import types + +from ._units import DAY, MONTH, YEAR + + +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 '{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 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"{type(types.Instant)}. {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"{type(int)}. {LEARN_MORE}" + ) diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index 4002c1e10e..aec62e0519 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -1,13 +1,16 @@ from __future__ import annotations -from typing import Any, Dict, NoReturn, Optional +from typing import Any, Optional import datetime -import os from openfisca_core import types -from .. import periods +from ._config import INSTANT_PATTERN +from ._errors import InstantFormatError, PeriodFormatError +from ._units import DAY, ETERNITY, MONTH, YEAR, UNIT_WEIGHTS +from .instant_ import Instant +from .period_ import Period def build_instant(value: Any) -> Optional[types.Instant]: @@ -21,16 +24,16 @@ def build_instant(value: Any) -> Optional[types.Instant]: :obj:`.Instant`: Otherwise. Raises: - ValueError: When the arguments were invalid, like "2021-32-13". + InstantFormatError: When the arguments were invalid, like "2021-32-13". Examples: >>> build_instant(datetime.date(2021, 9, 16)) Instant((2021, 9, 16)) - >>> build_instant(periods.Instant((2021, 9, 16))) + >>> build_instant(Instant((2021, 9, 16))) Instant((2021, 9, 16)) - >>> build_instant(periods.Period(("year", periods.Instant((2021, 9, 16)), 1))) + >>> build_instant(Period(("year", Instant((2021, 9, 16)), 1))) Instant((2021, 9, 16)) >>> build_instant("2021") @@ -47,23 +50,20 @@ def build_instant(value: Any) -> Optional[types.Instant]: if value is None: return None - if isinstance(value, periods.Instant): + if isinstance(value, Instant): return value if isinstance(value, str): - if not periods.INSTANT_PATTERN.match(value): - raise ValueError( - f"'{value}' is not a valid instant. Instants are described" - "using the 'YYYY-MM-DD' format, for instance '2015-06-15'." - ) + if not INSTANT_PATTERN.match(value): + raise InstantFormatError(value) - instant = periods.Instant( + instant = Instant( int(fragment) for fragment in value.split('-', 2)[:3] ) elif isinstance(value, datetime.date): - instant = periods.Instant((value.year, value.month, value.day)) + instant = Instant((value.year, value.month, value.day)) elif isinstance(value, int): instant = (value,) @@ -72,7 +72,7 @@ def build_instant(value: Any) -> Optional[types.Instant]: assert 1 <= len(value) <= 3 instant = tuple(value) - elif isinstance(value, periods.Period): + elif isinstance(value, Period): instant = value.start else: @@ -81,12 +81,12 @@ def build_instant(value: Any) -> Optional[types.Instant]: instant = value if len(instant) == 1: - return periods.Instant((instant[0], 1, 1)) + return Instant((instant[0], 1, 1)) if len(instant) == 2: - return periods.Instant((instant[0], instant[1], 1)) + return Instant((instant[0], instant[1], 1)) - return periods.Instant(instant) + return Instant(instant) def build_period(value: Any) -> types.Period: @@ -99,13 +99,13 @@ def build_period(value: Any) -> types.Period: :obj:`.Period`: A period. Raises: - :exc:`ValueError`: When the arguments were invalid, like "2021-32-13". + PeriodFormatError: When the arguments were invalid, like "2021-32-13". Examples: - >>> build_period(periods.Period(("year", periods.Instant((2021, 1, 1)), 1))) + >>> build_period(Period(("year", Instant((2021, 1, 1)), 1))) Period(('year', Instant((2021, 1, 1)), 1)) - >>> build_period(periods.Instant((2021, 1, 1))) + >>> build_period(Instant((2021, 1, 1))) Period(('day', Instant((2021, 1, 1)), 1)) >>> build_period("eternity") @@ -134,20 +134,20 @@ def build_period(value: Any) -> types.Period: """ - if isinstance(value, periods.Period): + if isinstance(value, Period): return value - if isinstance(value, periods.Instant): - return periods.Period((periods.DAY, value, 1)) + if isinstance(value, Instant): + return Period((DAY, value, 1)) - if value == "ETERNITY" or value == periods.ETERNITY: - return periods.Period(("eternity", build_instant(datetime.date.min), float("inf"))) + if value == "ETERNITY" or value == ETERNITY: + return Period(("eternity", build_instant(datetime.date.min), float("inf"))) if isinstance(value, int): - return periods.Period((periods.YEAR, periods.Instant((value, 1, 1)), 1)) + return Period((YEAR, Instant((value, 1, 1)), 1)) if not isinstance(value, str): - _raise_error(value) + raise PeriodFormatError(value) # Try to parse as a simple period period = parse_simple_period(value) @@ -157,21 +157,21 @@ def build_period(value: Any) -> types.Period: # Complex periods must have a ':' in their strings if ":" not in value: - _raise_error(value) + raise PeriodFormatError(value) components = value.split(":") # Left-most component must be a valid unit unit = components[0] - if unit not in (periods.DAY, periods.MONTH, periods.YEAR): - _raise_error(value) + if unit not in (DAY, MONTH, YEAR): + raise PeriodFormatError(value) # Middle component must be a valid iso period base_period = parse_simple_period(components[1]) if not base_period: - _raise_error(value) + raise PeriodFormatError(value) # Periods like year:2015-03 have a size of 1 if len(components) == 2: @@ -183,17 +183,17 @@ def build_period(value: Any) -> types.Period: size = int(components[2]) except ValueError: - _raise_error(value) + raise PeriodFormatError(value) # If there are more than 2 ":" in the string, the period is invalid else: - _raise_error(value) + raise PeriodFormatError(value) # Reject ambiguous periods such as month:2014 - if unit_weight(base_period.unit) > unit_weight(unit): - _raise_error(value) + if UNIT_WEIGHTS[base_period.unit] > UNIT_WEIGHTS[unit]: + raise PeriodFormatError(value) - return periods.Period((unit, base_period.start, size)) + return Period((unit, base_period.start, size)) def key_period_size(period: types.Period) -> str: @@ -208,13 +208,13 @@ def key_period_size(period: types.Period) -> str: :obj:`str`: A string. Examples: - >>> instant = periods.Instant((2021, 9, 14)) + >>> instant = Instant((2021, 9, 14)) - >>> period = periods.Period(("day", instant, 1)) + >>> period = Period(("day", instant, 1)) >>> key_period_size(period) '100_1' - >>> period = periods.Period(("year", instant, 3)) + >>> period = Period(("year", instant, 3)) >>> key_period_size(period) '300_3' @@ -222,13 +222,17 @@ def key_period_size(period: types.Period) -> str: unit, start, size = period - return f"{unit_weight(unit)}_{size}" + return f"{UNIT_WEIGHTS[unit]}_{size}" def parse_simple_period(value: str) -> Optional[types.Period]: """Parse simple periods respecting the ISO format. - Such as "2012" or "2015-03". + Args: + value: A string such as such as "2012" or "2015-03". + + Returns: + A Period. Examples: >>> parse_simple_period("2022") @@ -257,60 +261,10 @@ def parse_simple_period(value: str) -> Optional[types.Period]: return None else: - return periods.Period((periods.DAY, periods.Instant((date.year, date.month, date.day)), 1)) + return Period((DAY, Instant((date.year, date.month, date.day)), 1)) else: - return periods.Period((periods.MONTH, periods.Instant((date.year, date.month, 1)), 1)) + return Period((MONTH, Instant((date.year, date.month, 1)), 1)) else: - return periods.Period((periods.YEAR, periods.Instant((date.year, date.month, 1)), 1)) - - -def unit_weights() -> Dict[str, int]: - """Assign weights to date units. - - Examples: - >>> unit_weights() - {'day': 100, ...} - - """ - - return { - periods.DAY: 100, - periods.MONTH: 200, - periods.YEAR: 300, - periods.ETERNITY: 400, - } - - -def unit_weight(unit: str) -> int: - """Retrieves a specific date unit weight. - - Examples: - >>> unit_weight("day") - 100 - - """ - - return unit_weights()[unit] - - -def _raise_error(value: str) -> NoReturn: - """Raise an error. - - Examples: - >>> _raise_error("Oi mate!") - Traceback (most recent call last): - ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: - 'Oi mate!'. Learn more about legal period formats in OpenFisca: - . - - """ - - message = os.linesep.join([ - "Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got:", - f"'{value}'. Learn more about legal period formats in OpenFisca:", - "." - ]) - - raise ValueError(message) + return Period((YEAR, Instant((date.year, date.month, 1)), 1)) diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_units.py index 5b139ab0d0..fc243a6572 100644 --- a/openfisca_core/periods/_units.py +++ b/openfisca_core/periods/_units.py @@ -2,3 +2,10 @@ MONTH = "month" YEAR = "year" ETERNITY = "eternity" + +UNIT_WEIGHTS = { + DAY: 100, + MONTH: 200, + YEAR: 300, + ETERNITY: 400, + } diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index f2f5cba021..0e8eb8dd2f 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,13 +1,14 @@ from __future__ import annotations -from typing import Any, Dict, Optional, Union +from typing import Dict, Optional, Union import calendar import datetime from openfisca_core import types -from .. import periods +from ._errors import DateUnitValueError, InstantTypeError, OffsetTypeError +from ._units import DAY, MONTH, YEAR class Instant(tuple): @@ -84,7 +85,7 @@ def __str__(self) -> str: return str(self) @staticmethod - def to_date(value: Any) -> Optional[datetime.date]: + def to_date(value: Optional[types.Instant]) -> Optional[datetime.date]: """Returns the date representation of an ``Instant``. Args: @@ -95,18 +96,22 @@ def to_date(value: Any) -> Optional[datetime.date]: None: When ``value`` is None. datetime.date: Otherwise. + Raises: + InstantTypeError: When ``value`` is not an ``Instant``. + Examples: >>> Instant.to_date(Instant((2021, 1, 1))) datetime.date(2021, 1, 1) """ - instant = periods.build_instant(value) - - if instant is None: + if value is None: return None - return instant.date + if isinstance(value, types.Instant): + return value.date + + raise InstantTypeError(value) @property def year(self) -> int: @@ -190,9 +195,8 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: Instant: A new one. Raises: - AssertionError: When ``unit`` is not a date unit. - AssertionError: When ``offset`` is not either ``first-of``, - ``last-of``, or any ``int``. + DateUnitValueError: When ``unit`` is not a date unit. + OffsetTypeError: When ``offset`` is of type ``int``. Examples: >>> Instant((2020, 12, 31)).offset("first-of", "month") @@ -211,28 +215,30 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: year, month, day = self - assert unit in (periods.DAY, periods.MONTH, periods.YEAR), 'Invalid unit: {} of type {}'.format(unit, type(unit)) + if unit not in (DAY, MONTH, YEAR): + raise DateUnitValueError(unit) if offset == 'first-of': - if unit == periods.MONTH: + if unit == MONTH: day = 1 - elif unit == periods.YEAR: + elif unit == YEAR: month = 1 day = 1 elif offset == 'last-of': - if unit == periods.MONTH: + if unit == MONTH: day = calendar.monthrange(year, month)[1] - elif unit == periods.YEAR: + elif unit == YEAR: month = 12 day = 31 else: - assert isinstance(offset, int), 'Invalid offset: {} of type {}'.format(offset, type(offset)) + if not isinstance(offset, int): + raise OffsetTypeError(offset) - if unit == periods.DAY: + if unit == DAY: day += offset if offset < 0: @@ -258,7 +264,7 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: day -= month_last_day month_last_day = calendar.monthrange(year, month)[1] - elif unit == periods.MONTH: + elif unit == MONTH: month += offset if offset < 0: @@ -275,7 +281,7 @@ def offset(self, offset: Union[str, int], unit: str) -> types.Instant: if day > month_last_day: day = month_last_day - elif unit == periods.YEAR: + elif unit == YEAR: year += offset # Handle february month of leap year. diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 11e2f853ea..060fbfd201 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -7,7 +7,8 @@ from openfisca_core import types -from .. import periods +from ._units import DAY, MONTH, YEAR, ETERNITY, UNIT_WEIGHTS +from .instant_ import Instant class Period(tuple): @@ -28,8 +29,8 @@ class Period(tuple): The ``unit``, ``start``, and ``size``, accordingly. Examples: - >>> instant = periods.Instant((2021, 9, 1)) - >>> period = periods.Period((periods.YEAR, instant, 3)) + >>> instant = Instant((2021, 9, 1)) + >>> period = Period((YEAR, instant, 3)) ``Periods`` are represented as a ``tuple`` containing the ``unit``, an ``Instant`` and the ``size``: @@ -61,10 +62,10 @@ class Period(tuple): >>> len(period) 3 - >>> period == periods.Period(("year", instant, 3)) + >>> period == Period(("year", instant, 3)) True - >>> period > periods.Period(("year", instant, 3)) + >>> period > Period(("year", instant, 3)) False >>> unit, (year, month, day), size = period @@ -83,58 +84,58 @@ def __str__(self) -> str: str: A string representation of the period. Examples: - >>> str(Period(("year", periods.Instant((2021, 1, 1)), 1))) + >>> str(Period(("year", Instant((2021, 1, 1)), 1))) '2021' - >>> str(Period(("year", periods.Instant((2021, 2, 1)), 1))) + >>> str(Period(("year", Instant((2021, 2, 1)), 1))) 'year:2021-02' - >>> str(Period(("month", periods.Instant((2021, 2, 1)), 1))) + >>> str(Period(("month", Instant((2021, 2, 1)), 1))) '2021-02' - >>> str(Period(("year", periods.Instant((2021, 1, 1)), 2))) + >>> str(Period(("year", Instant((2021, 1, 1)), 2))) 'year:2021:2' - >>> str(Period(("month", periods.Instant((2021, 1, 1)), 2))) + >>> str(Period(("month", Instant((2021, 1, 1)), 2))) 'month:2021-01:2' - >>> str(Period(("month", periods.Instant((2021, 1, 1)), 12))) + >>> str(Period(("month", Instant((2021, 1, 1)), 12))) '2021' - >>> str(Period(("year", periods.Instant((2021, 3, 1)), 2))) + >>> str(Period(("year", Instant((2021, 3, 1)), 2))) 'year:2021-03:2' - >>> str(Period(("month", periods.Instant((2021, 3, 1)), 2))) + >>> str(Period(("month", Instant((2021, 3, 1)), 2))) 'month:2021-03:2' - >>> str(Period(("month", periods.Instant((2021, 3, 1)), 12))) + >>> str(Period(("month", Instant((2021, 3, 1)), 12))) 'year:2021-03' """ unit, start_instant, size = self - if unit == periods.ETERNITY: + if unit == ETERNITY: return "ETERNITY" year, month, day = start_instant # 1 year long period - if (unit == periods.MONTH and size == 12 or unit == periods.YEAR and size == 1): + if (unit == MONTH and size == 12 or unit == YEAR and size == 1): if month == 1: # civil year starting from january return str(year) else: # rolling year - return '{}:{}-{:02d}'.format(periods.YEAR, year, month) + return '{}:{}-{:02d}'.format(YEAR, year, month) # simple month - if unit == periods.MONTH and size == 1: + if unit == MONTH and size == 1: return '{}-{:02d}'.format(year, month) # several civil years - if unit == periods.YEAR and month == 1: + if unit == YEAR and month == 1: return '{}:{}:{}'.format(unit, year, size) - if unit == periods.DAY: + if unit == DAY: if size == 1: return '{}-{:02d}-{:02d}'.format(year, month, day) else: @@ -154,12 +155,12 @@ def date(self) -> datetime.date: ValueError: If the period's size is greater than 1. Examples: - >>> instant = periods.Instant((2021, 10, 1)) - >>> period = periods.Period((periods.YEAR, instant, 1)) + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((YEAR, instant, 1)) >>> period.date datetime.date(2021, 10, 1) - >>> period = periods.Period((periods.YEAR, instant, 3)) + >>> period = Period((YEAR, instant, 3)) >>> period.date Traceback (most recent call last): ValueError: "date" is undefined for a period of size > 1: year:2021-10:3. @@ -179,8 +180,8 @@ def unit(self) -> str: An int. Example: - >>> instant = periods.Instant((2021, 10, 1)) - >>> period = periods.Period((periods.YEAR, instant, 3)) + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((YEAR, instant, 3)) >>> period.unit 'year' @@ -196,12 +197,12 @@ def days(self) -> int: An int. Examples: - >>> instant = periods.Instant((2021, 10, 1)) - >>> period = periods.Period((periods.YEAR, instant, 3)) + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((YEAR, instant, 3)) >>> period.size_in_days 1096 - >>> period = periods.Period((periods.MONTH, instant, 3)) + >>> period = Period((MONTH, instant, 3)) >>> period.size_in_days 92 @@ -217,8 +218,8 @@ def size(self) -> int: An int. Example: - >>> instant = periods.Instant((2021, 10, 1)) - >>> period = periods.Period((periods.YEAR, instant, 3)) + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((YEAR, instant, 3)) >>> period.size 3 @@ -237,22 +238,22 @@ def size_in_months(self) -> int: ValueError: If the period's unit is not a month or a year. Examples: - >>> instant = periods.Instant((2021, 10, 1)) - >>> period = periods.Period((periods.YEAR, instant, 3)) + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((YEAR, instant, 3)) >>> period.size_in_months 36 - >>> period = periods.Period((periods.DAY, instant, 3)) + >>> period = Period((DAY, instant, 3)) >>> period.size_in_months Traceback (most recent call last): ValueError: Cannot calculate number of months in day. """ - if (self[0] == periods.MONTH): + if (self[0] == MONTH): return self[2] - if(self[0] == periods.YEAR): + if(self[0] == YEAR): return self[2] * 12 raise ValueError(f"Cannot calculate number of months in {self[0]}.") @@ -268,12 +269,12 @@ def size_in_days(self) -> int: ValueError: If the period's unit is not a day, a month or a year. Examples: - >>> instant = periods.Instant((2019, 10, 1)) - >>> period = periods.Period((periods.YEAR, instant, 3)) + >>> instant = Instant((2019, 10, 1)) + >>> period = Period((YEAR, instant, 3)) >>> period.size_in_days 1096 - >>> period = periods.Period((periods.MONTH, instant, 3)) + >>> period = Period((MONTH, instant, 3)) >>> period.size_in_days 92 @@ -281,11 +282,11 @@ def size_in_days(self) -> int: unit, instant, length = self - if unit == periods.DAY: + if unit == DAY: return length - if unit in [periods.MONTH, periods.YEAR]: - last_day = self.start.offset(length, unit).offset(-1, periods.DAY) + if unit in [MONTH, YEAR]: + last_day = self.start.offset(length, unit).offset(-1, DAY) return (last_day.date - self.start.date).days + 1 raise ValueError(f"Cannot calculate number of days in {unit}") @@ -298,8 +299,8 @@ def start(self) -> types.Instant: An Instant. Example: - >>> instant = periods.Instant((2021, 10, 1)) - >>> period = periods.Period((periods.YEAR, instant, 3)) + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((YEAR, instant, 3)) >>> period.start Instant((2021, 10, 1)) @@ -315,13 +316,13 @@ def stop(self) -> types.Instant: An Instant. Examples: - >>> periods.Period(("year", periods.Instant((2012, 2, 29)), 1)).stop + >>> Period(("year", Instant((2012, 2, 29)), 1)).stop Instant((2013, 2, 28)) - >>> periods.Period(("month", periods.Instant((2012, 2, 29)), 1)).stop + >>> Period(("month", Instant((2012, 2, 29)), 1)).stop Instant((2012, 3, 28)) - >>> periods.Period(("day", periods.Instant((2012, 2, 29)), 1)).stop + >>> Period(("day", Instant((2012, 2, 29)), 1)).stop Instant((2012, 2, 29)) """ @@ -329,8 +330,8 @@ def stop(self) -> types.Instant: unit, start_instant, size = self year, month, day = start_instant - if unit == periods.ETERNITY: - return periods.Instant((float("inf"), float("inf"), float("inf"))) + if unit == ETERNITY: + return Instant((float("inf"), float("inf"), float("inf"))) if unit == 'day': if size > 1: @@ -369,7 +370,7 @@ def stop(self) -> types.Instant: month = 1 day -= month_last_day - return periods.Instant((year, month, day)) + return Instant((year, month, day)) @property def last_month(self) -> types.Period: @@ -392,13 +393,13 @@ def last_3_months(self) -> types.Period: """ start: types.Instant = self.first_month.start - return self.__class__((periods.MONTH, start, 3)).offset(-3) + return self.__class__((MONTH, start, 3)).offset(-3) @property def last_year(self) -> types.Period: """Last year of the ``Period``.""" - start: types.Instant = self.start.offset("first-of", periods.YEAR) - return self.__class__((periods.YEAR, start, 1)).offset(-1) + start: types.Instant = self.start.offset("first-of", YEAR) + return self.__class__((YEAR, start, 1)).offset(-1) @property def n_2(self) -> types.Period: @@ -409,8 +410,8 @@ def n_2(self) -> types.Period: """ - start: types.Instant = self.start.offset("first-of", periods.YEAR) - return self.__class__((periods.YEAR, start, 1)).offset(-2) + start: types.Instant = self.start.offset("first-of", YEAR) + return self.__class__((YEAR, start, 1)).offset(-2) @property def this_year(self) -> types.Period: @@ -421,8 +422,8 @@ def this_year(self) -> types.Period: """ - start: types.Instant = self.start.offset("first-of", periods.YEAR) - return self.__class__((periods.YEAR, start, 1)) + start: types.Instant = self.start.offset("first-of", YEAR) + return self.__class__((YEAR, start, 1)) @property def first_month(self) -> types.Period: @@ -433,8 +434,8 @@ def first_month(self) -> types.Period: """ - start: types.Instant = self.start.offset("first-of", periods.MONTH) - return self.__class__((periods.MONTH, start, 1)) + start: types.Instant = self.start.offset("first-of", MONTH) + return self.__class__((MONTH, start, 1)) @property def first_day(self) -> types.Period: @@ -444,7 +445,7 @@ def first_day(self) -> types.Period: A Period. """ - return self.__class__((periods.DAY, self.start, 1)) + return self.__class__((DAY, self.start, 1)) def get_subperiods(self, unit: str) -> Sequence[types.Period]: """Return the list of all the periods of unit ``unit``. @@ -459,27 +460,27 @@ def get_subperiods(self, unit: str) -> Sequence[types.Period]: ValueError: If the period's unit is smaller than the given unit. Examples: - >>> period = periods.Period((periods.YEAR, periods.Instant((2021, 1, 1)), 1)) - >>> period.get_subperiods(periods.MONTH) + >>> period = Period((YEAR, Instant((2021, 1, 1)), 1)) + >>> period.get_subperiods(MONTH) [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] - >>> period = periods.Period((periods.YEAR, periods.Instant((2021, 1, 1)), 2)) - >>> period.get_subperiods(periods.YEAR) + >>> period = Period((YEAR, Instant((2021, 1, 1)), 2)) + >>> period.get_subperiods(YEAR) [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] """ - if periods.unit_weight(self.unit) < periods.unit_weight(unit): + if UNIT_WEIGHTS[self.unit] < UNIT_WEIGHTS[unit]: raise ValueError('Cannot subdivide {0} into {1}'.format(self.unit, unit)) - if unit == periods.YEAR: - return [self.this_year.offset(i, periods.YEAR) for i in range(self.size)] + if unit == YEAR: + return [self.this_year.offset(i, YEAR) for i in range(self.size)] - if unit == periods.MONTH: - return [self.first_month.offset(i, periods.MONTH) for i in range(self.size_in_months)] + if unit == MONTH: + return [self.first_month.offset(i, MONTH) for i in range(self.size_in_months)] - if unit == periods.DAY: - return [self.first_day.offset(i, periods.DAY) for i in range(self.size_in_days)] + if unit == DAY: + return [self.first_day.offset(i, DAY) for i in range(self.size_in_days)] def offset( self, @@ -496,16 +497,16 @@ def offset( Period: A new one. Examples: - >>> periods.Period(("day", periods.Instant((2014, 2, 3)), 1)).offset("first-of", "month") + >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("first-of", "month") Period(('day', Instant((2014, 2, 1)), 1)) - >>> periods.Period(("month", periods.Instant((2014, 2, 3)), 4)).offset("last-of", "month") + >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of", "month") Period(('month', Instant((2014, 2, 28)), 4)) - >>> periods.Period(("day", periods.Instant((2021, 1, 1)), 365)).offset(-3) + >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(-3) Period(('day', Instant((2020, 12, 29)), 365)) - >>> periods.Period(("day", periods.Instant((2021, 1, 1)), 365)).offset(1, "year") + >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "year") Period(('day', Instant((2022, 1, 1)), 365)) """ @@ -522,8 +523,8 @@ def contains(self, other: types.Period) -> bool: True if ``other`` is contained, otherwise False. Example: - >>> period = periods.Period((periods.YEAR, periods.Instant((2021, 1, 1)), 1)) - >>> sub_period = periods.Period((periods.MONTH, periods.Instant((2021, 1, 1)), 3)) + >>> period = Period((YEAR, Instant((2021, 1, 1)), 1)) + >>> sub_period = Period((MONTH, Instant((2021, 1, 1)), 3)) >>> period.contains(sub_period) True diff --git a/openfisca_core/periods/tests/test_helpers.py b/openfisca_core/periods/tests/test_helpers.py index 1951c7eeb6..a6523396a6 100644 --- a/openfisca_core/periods/tests/test_helpers.py +++ b/openfisca_core/periods/tests/test_helpers.py @@ -16,6 +16,7 @@ ["1000-01-01", periods.Instant((1000, 1, 1))], ]) def test_build_instant(arg, expected): + """Returns the expected ``Instant``.""" assert periods.build_instant(arg) == expected @@ -37,6 +38,8 @@ def test_build_instant(arg, expected): ["year:1000-01-01:3", 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) @@ -68,6 +71,7 @@ def test_build_instant_with_an_invalid_argument(arg, error): ["day:1000-01-01:3", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 3))], ]) def test_build_period(arg, expected): + """Returns the expected ``Period``.""" assert periods.build_period(arg) == expected @@ -90,6 +94,8 @@ def test_build_period(arg, expected): ["day:1000-01:1", 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) @@ -106,6 +112,7 @@ def test_build_period_with_an_invalid_argument(arg, error): ["1000-01-99", None], ]) def test_parse_simple_period(arg, expected): + """Returns an ``Instant`` when given a valid ISO format string.""" assert periods.parse_simple_period(arg) == expected @@ -116,4 +123,5 @@ def test_parse_simple_period(arg, expected): [periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1)), "400_1"], ]) def test_key_period_size_with_a_valid_argument(arg, expected): + """Returns the corresponding period's weight.""" assert periods.key_period_size(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index cc17e5bf8b..50f30efd99 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -7,6 +7,7 @@ @pytest.fixture def instant(): + """Returns a ``Instant``.""" return periods.Instant((2020, 2, 29)) @@ -14,13 +15,14 @@ def instant(): [None, None], [periods.Instant((1, 1, 1)), datetime.date(1, 1, 1)], [periods.Instant((4, 2, 29)), datetime.date(4, 2, 29)], - [(1, 1, 1), datetime.date(1, 1, 1)], ]) def test_to_date(arg, expected): + """Returns the expected ``date``.""" assert periods.Instant.to_date(arg) == expected @pytest.mark.parametrize("arg, error", [ + [(1, 1, 1), periods.InstantTypeError], [periods.Instant((-1, 1, 1)), ValueError], [periods.Instant((1, -1, 1)), ValueError], [periods.Instant((1, 1, -1)), ValueError], @@ -29,6 +31,8 @@ def test_to_date(arg, expected): [periods.Instant((1, 2, 29)), ValueError], ]) def test_to_date_with_an_invalid_argument(arg, error): + """Raises ``ValueError`` when given an invalid argument.""" + with pytest.raises(error): periods.Instant.to_date(arg) @@ -48,4 +52,5 @@ def test_to_date_with_an_invalid_argument(arg, error): [3, periods.DAY, periods.Instant((2020, 3, 3))], ]) def test_offset(instant, offset, unit, expected): + """Returns the expected ``Instant``.""" assert instant.offset(offset, unit) == expected diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 723c806ad3..5801740537 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -5,6 +5,7 @@ @pytest.fixture def instant(): + """Returns a ``Instant``.""" return periods.Instant((2022, 12, 31)) @@ -17,6 +18,7 @@ def instant(): [periods.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 @@ -26,6 +28,7 @@ def test_str_with_years(date_unit, instant, size, expected): [periods.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 @@ -35,6 +38,7 @@ def test_str_with_months(date_unit, instant, size, expected): [periods.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 @@ -47,8 +51,11 @@ def test_str_with_days(date_unit, instant, size, expected): [periods.DAY, periods.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.get_subperiods(unit) + assert len(subperiods) == count assert subperiods[0] == periods.Period((unit, start, 1)) assert subperiods[-1] == periods.Period((unit, cease, 1)) @@ -78,7 +85,10 @@ def test_subperiods(instant, period_unit, unit, start, cease, count): [periods.DAY, 3, periods.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 @@ -92,7 +102,10 @@ def test_offset(instant, period_unit, offset, unit, expected): [periods.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 @@ -108,5 +121,8 @@ def test_day_size_in_months(date_unit, instant, size, expected): [periods.YEAR, periods.Instant((2022, 1, 1)), 2, 730], ]) 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 diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 17cb73ebf7..2c207c93e2 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -171,7 +171,7 @@ def calculate_add(self, variable_name: str, 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, diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types/_domain.py index 951e752b1c..48452e403e 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types/_domain.py @@ -55,6 +55,7 @@ def get_memory_usage(self) -> Any: """Abstract method.""" +@typing_extensions.runtime_checkable class Instant(Protocol): """Instant protocol.""" diff --git a/setup.cfg b/setup.cfg index d66ba84ea3..dfa559dd75 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,7 +55,7 @@ ignore_errors = True ignore_errors = True [mypy-openfisca_core.periods.tests.*] -ignore_errors = True +ignore_errors = True [mypy-openfisca_core.scripts.*] ignore_errors = True diff --git a/tests/web_api/test_calculate.py b/tests/web_api/test_calculate.py index 0fcc67f0b7..a12abb0363 100644 --- a/tests/web_api/test_calculate.py +++ b/tests/web_api/test_calculate.py @@ -41,8 +41,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 +268,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 From 6eb90a2027e1897be1250279cbf2086afd6c6e42 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 05:51:24 +0100 Subject: [PATCH 32/93] Make stricter --- CHANGELOG.md | 1 + openfisca_core/periods/__init__.py | 6 +- openfisca_core/periods/_funcs.py | 99 ++++++++++++++----- .../tests/{test_helpers.py => test_funcs.py} | 14 +-- openfisca_core/periods/tests/test_instant.py | 2 +- setup.py | 8 +- 6 files changed, 88 insertions(+), 42 deletions(-) rename openfisca_core/periods/tests/{test_helpers.py => test_funcs.py} (91%) diff --git a/CHANGELOG.md b/CHANGELOG.md index afd5ba81f5..ca109d572d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Rename `instant` to `build_instant`. - Rename `period` to `build_period`. - Move `instant_date` to `Instant.to_date`. +- Make `periods.parse_period` stricter (for example `2022-1` now fails). #### Technical changes diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 3ba24cf9db..d2efda1d34 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -31,15 +31,11 @@ INSTANT_PATTERN, ) -from ._errors import ( # noqa: F401 - InstantTypeError, - ) - from ._funcs import ( # noqa: F401 build_instant, build_period, key_period_size, - parse_simple_period, + parse_period, ) from ._units import ( # noqa: F401 diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index aec62e0519..4f27be831b 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -4,6 +4,10 @@ import datetime +import pendulum +from pendulum.datetime import Date +from pendulum.parsing import ParserError + from openfisca_core import types from ._config import INSTANT_PATTERN @@ -120,16 +124,16 @@ def build_period(value: Any) -> types.Period: >>> build_period("year:2014") Period(('year', Instant((2014, 1, 1)), 1)) - >>> build_period("month:2014-2") + >>> build_period("month:2014-02") Period(('month', Instant((2014, 2, 1)), 1)) - >>> build_period("year:2014-2") + >>> build_period("year:2014-02") Period(('year', Instant((2014, 2, 1)), 1)) - >>> build_period("day:2014-2-2") + >>> build_period("day:2014-02-02") Period(('day', Instant((2014, 2, 2)), 1)) - >>> build_period("day:2014-2-2:3") + >>> build_period("day:2014-02-02:3") Period(('day', Instant((2014, 2, 2)), 3)) """ @@ -150,7 +154,7 @@ def build_period(value: Any) -> types.Period: raise PeriodFormatError(value) # Try to parse as a simple period - period = parse_simple_period(value) + period = parse_period(value) if period is not None: return period @@ -168,7 +172,7 @@ def build_period(value: Any) -> types.Period: raise PeriodFormatError(value) # Middle component must be a valid iso period - base_period = parse_simple_period(components[1]) + base_period = parse_period(components[1]) if not base_period: raise PeriodFormatError(value) @@ -225,8 +229,8 @@ def key_period_size(period: types.Period) -> str: return f"{UNIT_WEIGHTS[unit]}_{size}" -def parse_simple_period(value: str) -> Optional[types.Period]: - """Parse simple periods respecting the ISO format. +def parse_period(value: str) -> Optional[types.Period]: + """Parse periods respecting the ISO format. Args: value: A string such as such as "2012" or "2015-03". @@ -234,37 +238,80 @@ def parse_simple_period(value: str) -> Optional[types.Period]: Returns: A Period. + Raises: + AttributeError: When arguments are invalid, like ``"-1"``. + ValueError: When values are invalid, like ``"2022-32-13"``. + Examples: - >>> parse_simple_period("2022") + >>> parse_period("2022") Period(('year', Instant((2022, 1, 1)), 1)) - >>> parse_simple_period("2022-02") + >>> parse_period("2022-02") Period(('month', Instant((2022, 2, 1)), 1)) - >>> parse_simple_period("2022-02-13") + >>> 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, next! + if value[0] == "-": + raise ValueError + + unit = _parse_unit(value) + try: - date = datetime.datetime.strptime(value, '%Y') + date = pendulum.parse(value, exact = True) - except ValueError: - try: - date = datetime.datetime.strptime(value, '%Y-%m') + except ParserError: + return None - except ValueError: - try: - date = datetime.datetime.strptime(value, '%Y-%m-%d') + if not isinstance(date, Date): + raise ValueError - except ValueError: - return None + instant = Instant((date.year, date.month, date.day)) - else: - return Period((DAY, Instant((date.year, date.month, date.day)), 1)) + return Period((unit, instant, 1)) - else: - return Period((MONTH, Instant((date.year, date.month, 1)), 1)) - else: - return Period((YEAR, Instant((date.year, date.month, 1)), 1)) +def _parse_unit(value: str) -> str: + """Determine the date unit of a date string. + + Args: + value (str): The date string to parse. + + Returns: + A date unit. + + Raises: + ValueError: when no date unit can be determined. + + Examples: + >>> _parse_unit("2022") + 'year' + + >>> _parse_unit("2022-03-01") + 'day' + + """ + + length = len(value.split("-")) + + if length == 1: + return YEAR + + elif length == 2: + return MONTH + + elif length == 3: + return DAY + + raise ValueError diff --git a/openfisca_core/periods/tests/test_helpers.py b/openfisca_core/periods/tests/test_funcs.py similarity index 91% rename from openfisca_core/periods/tests/test_helpers.py rename to openfisca_core/periods/tests/test_funcs.py index a6523396a6..b2fbd59cd8 100644 --- a/openfisca_core/periods/tests/test_helpers.py +++ b/openfisca_core/periods/tests/test_funcs.py @@ -52,8 +52,6 @@ def test_build_instant_with_an_invalid_argument(arg, error): [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365))], [1000, periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["1000-1", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["1000-1-1", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], ["1004-02-29", periods.Period((periods.DAY, periods.Instant((1004, 2, 29)), 1))], @@ -81,10 +79,12 @@ def test_build_period(arg, expected): [datetime.date(1, 1, 1), ValueError], ["1000:1", ValueError], ["1000-0", ValueError], + ["1000-1", ValueError], ["1000-13", ValueError], ["1000-01:1", ValueError], ["1000-0-0", ValueError], ["1000-1-0", ValueError], + ["1000-1-1", ValueError], ["1000-2-31", ValueError], ["1000-01-01:1", ValueError], ["month:1000", ValueError], @@ -104,16 +104,16 @@ def test_build_period_with_an_invalid_argument(arg, error): ["1", None], ["999", None], ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["1000-1", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["1000-1-1", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], - ["1000-01-1", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["1000-1", None], + ["1000-1-1", None], + ["1000-01-1", None], ["1000-01-99", None], ]) -def test_parse_simple_period(arg, expected): +def test_parse_period(arg, expected): """Returns an ``Instant`` when given a valid ISO format string.""" - assert periods.parse_simple_period(arg) == expected + assert periods.parse_period(arg) == expected @pytest.mark.parametrize("arg, expected", [ diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 50f30efd99..63265e2766 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -22,7 +22,7 @@ def test_to_date(arg, expected): @pytest.mark.parametrize("arg, error", [ - [(1, 1, 1), periods.InstantTypeError], + [(1, 1, 1), TypeError], [periods.Instant((-1, 1, 1)), ValueError], [periods.Instant((1, -1, 1)), ValueError], [periods.Instant((1, 1, -1)), ValueError], diff --git a/setup.py b/setup.py index 2355296ea0..7be9792e5f 100644 --- a/setup.py +++ b/setup.py @@ -26,13 +26,15 @@ # functional and integration breaks caused by external code updates. general_requirements = [ + 'PyYAML >= 3.10', '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', ] @@ -56,7 +58,7 @@ 'flake8-rst-docstrings == 0.2.3', 'mypy == 0.910', 'openapi-spec-validator >= 0.3.0', - 'pycodestyle >= 2.7.0, < 2.8.0', + 'pycodestyle >= 2.8.0, < 2.9.0', 'pylint == 2.10.2', 'xdoctest >= 1.0.0, < 2.0.0', ] + api_requirements From 31c2d321b9a1d6bd72f2a57c269f2a1bee213bc5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 06:09:08 +0100 Subject: [PATCH 33/93] Refactor _parse_unit --- openfisca_core/periods/_funcs.py | 49 +++++--------------------------- 1 file changed, 7 insertions(+), 42 deletions(-) diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index 4f27be831b..3ecb6f2f3d 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -16,6 +16,8 @@ from .instant_ import Instant from .period_ import Period +UNIT_MAPPING = {1: "year", 2: "month", 3: "day"} + def build_instant(value: Any) -> Optional[types.Instant]: """Build a new instant, aka a triple of integers (year, month, day). @@ -262,12 +264,10 @@ def parse_period(value: str) -> Optional[types.Period]: if not (value and isinstance(value, str)): raise AttributeError - # If it's negative, next! - if value[0] == "-": + # If it's negative period, next! + if value[0] == "-" or len(value.split(":")) != 1: raise ValueError - unit = _parse_unit(value) - try: date = pendulum.parse(value, exact = True) @@ -277,41 +277,6 @@ def parse_period(value: str) -> Optional[types.Period]: if not isinstance(date, Date): raise ValueError - instant = Instant((date.year, date.month, date.day)) - - return Period((unit, instant, 1)) - - -def _parse_unit(value: str) -> str: - """Determine the date unit of a date string. - - Args: - value (str): The date string to parse. - - Returns: - A date unit. - - Raises: - ValueError: when no date unit can be determined. - - Examples: - >>> _parse_unit("2022") - 'year' - - >>> _parse_unit("2022-03-01") - 'day' - - """ - - length = len(value.split("-")) - - if length == 1: - return YEAR - - elif length == 2: - return MONTH - - elif length == 3: - return DAY - - raise ValueError + unit = UNIT_MAPPING[len(value.split("-"))] + start = Instant((date.year, date.month, date.day)) + return Period((unit, start, 1)) From 9a96aed4659ca34f9cff5b441d8fa89fd70b132a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 07:31:00 +0100 Subject: [PATCH 34/93] Refactor as --- CHANGELOG.md | 1 + .../data_storage/in_memory_storage.py | 2 +- .../data_storage/on_disk_storage.py | 2 +- openfisca_core/periods/period_.py | 43 ++++++++++--------- openfisca_core/variables/helpers.py | 2 +- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca109d572d..09b367ff5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Rename `instant` to `build_instant`. - Rename `period` to `build_period`. - Move `instant_date` to `Instant.to_date`. +- Refactor `Period.contains` as `Period.__contains__`. - Make `periods.parse_period` stricter (for example `2022-1` now fails). #### Technical changes diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py index 765a6f836c..d5dc9e2f63 100644 --- a/openfisca_core/data_storage/in_memory_storage.py +++ b/openfisca_core/data_storage/in_memory_storage.py @@ -41,7 +41,7 @@ def delete(self, period = None): 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 402b576e6a..f3ba18ebb3 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -62,7 +62,7 @@ def delete(self, period = 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): diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 060fbfd201..22fa82412f 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -144,6 +144,29 @@ def __str__(self) -> str: # complex period return '{}:{}-{:02d}:{}'.format(unit, year, month, 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((YEAR, Instant((2021, 1, 1)), 1)) + >>> sub_period = Period((MONTH, Instant((2021, 1, 1)), 3)) + + >>> sub_period in period + True + + """ + + if isinstance(other, types.Period): + return self.start <= other.start and self.stop >= other.stop + + return super().__contains__(other) + @property def date(self) -> datetime.date: """The date representation of the ``period``'s' start date. @@ -512,23 +535,3 @@ def offset( """ return self.__class__((self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2])) - - def contains(self, other: types.Period) -> bool: - """Checks if a ``period`` contains another one. - - Args: - other (:obj:`.Period`): The other ``Period``. - - Returns: - True if ``other`` is contained, otherwise False. - - Example: - >>> period = Period((YEAR, Instant((2021, 1, 1)), 1)) - >>> sub_period = Period((MONTH, Instant((2021, 1, 1)), 3)) - - >>> period.contains(sub_period) - True - - """ - - return self.start <= other.start and self.stop >= other.stop diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index 335a585498..7ae026bb99 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -17,7 +17,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) From 1b79d3cd57506cf4a7b2c22c29814c10ad870740 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 10:17:05 +0100 Subject: [PATCH 35/93] Fix typing --- openfisca_core/periods/_config.py | 2 +- openfisca_core/periods/_funcs.py | 23 +++--- openfisca_core/periods/instant_.py | 10 +-- openfisca_core/periods/period_.py | 82 ++++++++++--------- .../taxbenefitsystems/tax_benefit_system.py | 13 ++- openfisca_core/types/_domain.py | 11 ++- openfisca_core/variables/variable.py | 9 +- 7 files changed, 79 insertions(+), 71 deletions(-) diff --git a/openfisca_core/periods/_config.py b/openfisca_core/periods/_config.py index 485f5fd44a..f3d3cf2b2a 100644 --- a/openfisca_core/periods/_config.py +++ b/openfisca_core/periods/_config.py @@ -6,4 +6,4 @@ # Matches "2015", "2015-01", "2015-01-01" # Does not match "2015-13", "2015-12-32" -INSTANT_PATTERN: Pattern = re.compile(r"^\d{4}(-(0[1-9]|1[012]))?(-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01]))?$") +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]))?$") diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index 3ecb6f2f3d..87c87d376d 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -59,28 +59,25 @@ def build_instant(value: Any) -> Optional[types.Instant]: if isinstance(value, Instant): return value - if isinstance(value, str): - if not INSTANT_PATTERN.match(value): - raise InstantFormatError(value) + if isinstance(value, Period): + return value.start - instant = Instant( - int(fragment) - for fragment in value.split('-', 2)[:3] - ) + 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 = Instant((value.year, value.month, value.day)) + instant = value.year, value.month, value.day elif isinstance(value, int): - instant = (value,) + instant = value, elif isinstance(value, list): assert 1 <= len(value) <= 3 instant = tuple(value) - elif isinstance(value, Period): - instant = value.start - else: assert isinstance(value, tuple), value assert 1 <= len(value) <= 3 @@ -226,7 +223,7 @@ def key_period_size(period: types.Period) -> str: """ - unit, start, size = period + unit, _start, size = period return f"{UNIT_WEIGHTS[unit]}_{size}" diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 0e8eb8dd2f..0d4c7e5fc1 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -5,13 +5,11 @@ import calendar import datetime -from openfisca_core import types - from ._errors import DateUnitValueError, InstantTypeError, OffsetTypeError from ._units import DAY, MONTH, YEAR -class Instant(tuple): +class Instant(tuple[int, int, int]): """An instant in time (``year``, ``month``, ``day``). An ``Instant`` represents the most atomic and indivisible @@ -85,7 +83,7 @@ def __str__(self) -> str: return str(self) @staticmethod - def to_date(value: Optional[types.Instant]) -> Optional[datetime.date]: + def to_date(value: Optional[Instant]) -> Optional[datetime.date]: """Returns the date representation of an ``Instant``. Args: @@ -108,7 +106,7 @@ def to_date(value: Optional[types.Instant]) -> Optional[datetime.date]: if value is None: return None - if isinstance(value, types.Instant): + if isinstance(value, Instant): return value.date raise InstantTypeError(value) @@ -184,7 +182,7 @@ def date(self) -> datetime.date: return self.date - def offset(self, offset: Union[str, int], unit: str) -> types.Instant: + def offset(self, offset: Union[str, int], unit: str) -> Instant: """Increments/decrements the given instant with offset units. Args: diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 22fa82412f..60eb690d32 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -5,13 +5,12 @@ import calendar import datetime -from openfisca_core import types - +from ._errors import DateUnitValueError from ._units import DAY, MONTH, YEAR, ETERNITY, UNIT_WEIGHTS from .instant_ import Instant -class Period(tuple): +class Period(tuple[str, Instant, int]): """Toolbox to handle date intervals. A ``Period`` is a triple (``unit``, ``start``, ``size``). @@ -121,28 +120,28 @@ def __str__(self) -> str: year, month, day = start_instant # 1 year long period - if (unit == MONTH and size == 12 or unit == YEAR and size == 1): + if unit == MONTH and size == 12 or unit == YEAR and size == 1: if month == 1: # civil year starting from january return str(year) else: # rolling year - return '{}:{}-{:02d}'.format(YEAR, year, month) + return f'{YEAR}:{year}-{month:02d}' # simple month if unit == MONTH and size == 1: - return '{}-{:02d}'.format(year, month) + return f'{year}-{month:02d}' # several civil years if unit == YEAR and month == 1: - return '{}:{}:{}'.format(unit, year, size) + return f'{unit}:{year}:{size}' if unit == DAY: if size == 1: - return '{}-{:02d}-{:02d}'.format(year, month, day) + return f'{year}-{month:02d}-{day:02d}' else: - return '{}:{}-{:02d}-{:02d}:{}'.format(unit, year, month, day, size) + return f'{unit}:{year}-{month:02d}-{day:02d}:{size}' # complex period - return '{}:{}-{:02d}:{}'.format(unit, year, month, size) + return f'{unit}:{year}-{month:02d}:{size}' def __contains__(self, other: object) -> bool: """Checks if a ``period`` contains another one. @@ -162,7 +161,7 @@ def __contains__(self, other: object) -> bool: """ - if isinstance(other, types.Period): + if isinstance(other, Period): return self.start <= other.start and self.stop >= other.stop return super().__contains__(other) @@ -273,10 +272,10 @@ def size_in_months(self) -> int: """ - if (self[0] == MONTH): + if self[0] == MONTH: return self[2] - if(self[0] == YEAR): + if self[0] == YEAR: return self[2] * 12 raise ValueError(f"Cannot calculate number of months in {self[0]}.") @@ -315,7 +314,7 @@ def size_in_days(self) -> int: raise ValueError(f"Cannot calculate number of days in {unit}") @property - def start(self) -> types.Instant: + def start(self) -> Instant: """The ``Instant`` at which the ``Period`` starts. Returns: @@ -332,7 +331,7 @@ def start(self) -> types.Instant: return self[1] @property - def stop(self) -> types.Instant: + def stop(self) -> Instant: """Last day of the ``Period`` as an ``Instant``. Returns: @@ -354,7 +353,7 @@ def stop(self) -> types.Instant: year, month, day = start_instant if unit == ETERNITY: - return Instant((float("inf"), float("inf"), float("inf"))) + return Instant((1, 1, 1)) if unit == 'day': if size > 1: @@ -375,7 +374,7 @@ def stop(self) -> types.Instant: year += 1 month -= 12 else: - assert unit == 'year', 'Invalid unit: {} of type {}'.format(unit, type(unit)) + assert unit == 'year', f'Invalid unit: {unit} of type {type(unit)}' year += size day -= 1 if day < 1: @@ -396,7 +395,7 @@ def stop(self) -> types.Instant: return Instant((year, month, day)) @property - def last_month(self) -> types.Period: + def last_month(self) -> Period: """Last month of the ``Period``. Returns: @@ -407,7 +406,7 @@ def last_month(self) -> types.Period: return self.first_month.offset(-1) @property - def last_3_months(self) -> types.Period: + def last_3_months(self) -> Period: """Last 3 months of the ``Period``. Returns: @@ -415,17 +414,17 @@ def last_3_months(self) -> types.Period: """ - start: types.Instant = self.first_month.start - return self.__class__((MONTH, start, 3)).offset(-3) + start: Instant = self.first_month.start + return Period((MONTH, start, 3)).offset(-3) @property - def last_year(self) -> types.Period: + def last_year(self) -> Period: """Last year of the ``Period``.""" - start: types.Instant = self.start.offset("first-of", YEAR) - return self.__class__((YEAR, start, 1)).offset(-1) + start: Instant = self.start.offset("first-of", YEAR) + return Period((YEAR, start, 1)).offset(-1) @property - def n_2(self) -> types.Period: + def n_2(self) -> Period: """Last 2 years of the ``Period``. Returns: @@ -433,11 +432,11 @@ def n_2(self) -> types.Period: """ - start: types.Instant = self.start.offset("first-of", YEAR) - return self.__class__((YEAR, start, 1)).offset(-2) + start: Instant = self.start.offset("first-of", YEAR) + return Period((YEAR, start, 1)).offset(-2) @property - def this_year(self) -> types.Period: + def this_year(self) -> Period: """A new year ``Period`` starting at the beginning of the year. Returns: @@ -445,11 +444,11 @@ def this_year(self) -> types.Period: """ - start: types.Instant = self.start.offset("first-of", YEAR) - return self.__class__((YEAR, start, 1)) + start: Instant = self.start.offset("first-of", YEAR) + return Period((YEAR, start, 1)) @property - def first_month(self) -> types.Period: + def first_month(self) -> Period: """A new month ``Period`` starting at the first of the month. Returns: @@ -457,29 +456,30 @@ def first_month(self) -> types.Period: """ - start: types.Instant = self.start.offset("first-of", MONTH) - return self.__class__((MONTH, start, 1)) + start: Instant = self.start.offset("first-of", MONTH) + return Period((MONTH, start, 1)) @property - def first_day(self) -> types.Period: + def first_day(self) -> Period: """A new day ``Period``. Returns: A Period. """ - return self.__class__((DAY, self.start, 1)) + return Period((DAY, self.start, 1)) - def get_subperiods(self, unit: str) -> Sequence[types.Period]: + def get_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. + 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: @@ -494,7 +494,7 @@ def get_subperiods(self, unit: str) -> Sequence[types.Period]: """ if UNIT_WEIGHTS[self.unit] < UNIT_WEIGHTS[unit]: - raise ValueError('Cannot subdivide {0} into {1}'.format(self.unit, unit)) + raise ValueError(f"Cannot subdivide {self.unit} into {unit}") if unit == YEAR: return [self.this_year.offset(i, YEAR) for i in range(self.size)] @@ -505,11 +505,13 @@ def get_subperiods(self, unit: str) -> Sequence[types.Period]: if unit == DAY: return [self.first_day.offset(i, DAY) for i in range(self.size_in_days)] + raise DateUnitValueError(unit) + def offset( self, offset: Union[str, int], unit: Optional[str] = None, - ) -> types.Period: + ) -> Period: """Increment (or decrement) the given period with offset units. Args: @@ -534,4 +536,4 @@ def offset( """ - return self.__class__((self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2])) + return Period((self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2])) diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 4af239ca52..cfb1245b0f 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -14,14 +14,13 @@ import traceback import typing -from openfisca_core import commons, periods, variables +from openfisca_core import commons, periods, types, variables from openfisca_core.entities import Entity 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.simulations import SimulationBuilder -from openfisca_core.types import ParameterNodeAtInstant from openfisca_core.variables import Variable log = logging.getLogger(__name__) @@ -43,7 +42,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. @@ -385,8 +384,8 @@ def _get_baseline_parameters_at_instant(self, instant): @functools.lru_cache() def get_parameters_at_instant( self, - instant: Union[str, int, Period, Instant], - ) -> Optional[ParameterNodeAtInstant]: + instant: Union[str, int, types.Period, types.Instant], + ) -> Optional[types.ParameterNodeAtInstant]: """Get the parameters of the legislation at a given instant Args: @@ -397,7 +396,7 @@ def get_parameters_at_instant( """ - key: Instant + key: Optional[types.Instant] msg: str if isinstance(instant, Instant): @@ -410,7 +409,7 @@ def get_parameters_at_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/types/_domain.py b/openfisca_core/types/_domain.py index 48452e403e..7eed350a78 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types/_domain.py @@ -86,9 +86,8 @@ def __call__(self, instant: Instant) -> ParameterNodeAtInstant: class Period(Protocol): """Period protocol.""" - @property @abc.abstractmethod - def start(self) -> Any: + def __iter__(self) -> Any: """Abstract method.""" @property @@ -96,14 +95,20 @@ def start(self) -> Any: def unit(self) -> Any: """Abstract method.""" + @property @abc.abstractmethod - def offset(self, offset: Any, unit: Any = None) -> Any: + def start(self) -> Any: """Abstract method.""" + @property @abc.abstractmethod def stop(self) -> Any: """Abstract method.""" + @abc.abstractmethod + def offset(self, offset: Any, unit: Any = None) -> Any: + """Abstract method.""" + class Population(Protocol): """Population protocol.""" diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index eeb9c33169..be2ea03aad 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -326,6 +326,8 @@ def get_formula( """ + instant: Optional[Instant] + if not self.formulas: return None @@ -334,11 +336,16 @@ def get_formula( if isinstance(period, Period): instant = period.start + else: try: 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: return None From 15a86783d57d19bbac8c3f7829dc427a2bfbb9d7 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 10:50:20 +0100 Subject: [PATCH 36/93] Fix typing in py3.7 --- openfisca_core/periods/instant_.py | 4 ++-- openfisca_core/periods/period_.py | 4 ++-- openfisca_core/taxbenefitsystems/tax_benefit_system.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 0d4c7e5fc1..1aacd46dcb 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Dict, Optional, Union +from typing import Dict, Optional, Tuple, Union import calendar import datetime @@ -9,7 +9,7 @@ from ._units import DAY, MONTH, YEAR -class Instant(tuple[int, int, int]): +class Instant(Tuple[int, int, int]): """An instant in time (``year``, ``month``, ``day``). An ``Instant`` represents the most atomic and indivisible diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 60eb690d32..ea7f1fc341 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Sequence, Union +from typing import Optional, Sequence, Tuple, Union import calendar import datetime @@ -10,7 +10,7 @@ from .instant_ import Instant -class Period(tuple[str, Instant, int]): +class Period(Tuple[str, Instant, int]): """Toolbox to handle date intervals. A ``Period`` is a triple (``unit``, ``start``, ``size``). diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index cfb1245b0f..35a90262d9 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -399,10 +399,10 @@ def get_parameters_at_instant( key: Optional[types.Instant] msg: str - if isinstance(instant, Instant): + if isinstance(instant, types.Instant): key = instant - elif isinstance(instant, Period): + elif isinstance(instant, types.Period): key = instant.start elif isinstance(instant, (str, int)): From c56c2a24d5f37249e054b65367ee76532eea6daa Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 11:00:04 +0100 Subject: [PATCH 37/93] Rename period & instant --- openfisca_core/periods/__init__.py | 4 ++-- openfisca_core/periods/_funcs.py | 4 ++-- openfisca_core/periods/{instant_.py => instant.py} | 0 openfisca_core/periods/{period_.py => period.py} | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename openfisca_core/periods/{instant_.py => instant.py} (100%) rename openfisca_core/periods/{period_.py => period.py} (99%) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index d2efda1d34..8f0f18fbcc 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -46,5 +46,5 @@ UNIT_WEIGHTS, ) -from .instant_ import Instant # noqa: F401 -from .period_ import Period # noqa: F401 +from .instant import Instant # noqa: F401 +from .period import Period # noqa: F401 diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index 87c87d376d..2431045dca 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -13,8 +13,8 @@ from ._config import INSTANT_PATTERN from ._errors import InstantFormatError, PeriodFormatError from ._units import DAY, ETERNITY, MONTH, YEAR, UNIT_WEIGHTS -from .instant_ import Instant -from .period_ import Period +from .instant import Instant +from .period import Period UNIT_MAPPING = {1: "year", 2: "month", 3: "day"} diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant.py similarity index 100% rename from openfisca_core/periods/instant_.py rename to openfisca_core/periods/instant.py diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period.py similarity index 99% rename from openfisca_core/periods/period_.py rename to openfisca_core/periods/period.py index ea7f1fc341..5edfde84ee 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period.py @@ -7,7 +7,7 @@ from ._errors import DateUnitValueError from ._units import DAY, MONTH, YEAR, ETERNITY, UNIT_WEIGHTS -from .instant_ import Instant +from .instant import Instant class Period(Tuple[str, Instant, int]): From 51e23123b67ae025d406a1f8f047e6d49b94fddb Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 11:22:32 +0100 Subject: [PATCH 38/93] Fix imports --- openfisca_core/periods/__init__.py | 22 +++------------------- openfisca_core/periods/_errors.py | 1 - openfisca_core/periods/_funcs.py | 2 +- openfisca_core/periods/period.py | 2 +- setup.cfg | 3 --- 5 files changed, 5 insertions(+), 25 deletions(-) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 8f0f18fbcc..5185fc9110 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -27,24 +27,8 @@ """ -from ._config import ( # noqa: F401 - INSTANT_PATTERN, - ) - -from ._funcs import ( # noqa: F401 - build_instant, - build_period, - key_period_size, - parse_period, - ) - -from ._units import ( # noqa: F401 - DAY, - ETERNITY, - MONTH, - YEAR, - UNIT_WEIGHTS, - ) - +from ._config import INSTANT_PATTERN # noqa: F401 +from ._funcs import build_instant, build_period, key_period_size, parse_period # noqa: F401 +from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR # noqa: F401 from .instant import Instant # noqa: F401 from .period import Period # noqa: F401 diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index b412fffda0..3d3e06936b 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -6,7 +6,6 @@ from ._units import DAY, MONTH, YEAR - LEARN_MORE = ( "Learn more about legal period formats in OpenFisca: " "." diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index 2431045dca..99a3f6a2bf 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -12,7 +12,7 @@ from ._config import INSTANT_PATTERN from ._errors import InstantFormatError, PeriodFormatError -from ._units import DAY, ETERNITY, MONTH, YEAR, UNIT_WEIGHTS +from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR from .instant import Instant from .period import Period diff --git a/openfisca_core/periods/period.py b/openfisca_core/periods/period.py index 5edfde84ee..fe73c94e8c 100644 --- a/openfisca_core/periods/period.py +++ b/openfisca_core/periods/period.py @@ -6,7 +6,7 @@ import datetime from ._errors import DateUnitValueError -from ._units import DAY, MONTH, YEAR, ETERNITY, UNIT_WEIGHTS +from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR from .instant import Instant diff --git a/setup.cfg b/setup.cfg index dfa559dd75..71851433ae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,8 +54,5 @@ ignore_errors = True [mypy-openfisca_core.holders.tests.*] ignore_errors = True -[mypy-openfisca_core.periods.tests.*] -ignore_errors = True - [mypy-openfisca_core.scripts.*] ignore_errors = True From baad937a0559a2977a3360228946d14c1aa734ee Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 12:20:14 +0100 Subject: [PATCH 39/93] Encapsulate periods --- openfisca_core/holders/holder.py | 21 +++++----- openfisca_core/periods/__init__.py | 10 ++--- openfisca_core/periods/_errors.py | 5 +-- openfisca_core/periods/_funcs.py | 10 ++--- openfisca_core/periods/typing.py | 40 +++++++++++++++++++ openfisca_core/populations/population.py | 3 +- .../taxbenefitsystems/tax_benefit_system.py | 8 ++-- openfisca_core/types/__init__.py | 12 +----- openfisca_core/types/_data.py | 4 +- openfisca_core/types/_domain.py | 21 ++-------- openfisca_core/variables/variable.py | 3 +- setup.cfg | 8 ++-- 12 files changed, 80 insertions(+), 65 deletions(-) create mode 100644 openfisca_core/periods/typing.py diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index 18784b0b91..8d29106acf 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -8,15 +8,12 @@ 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 openfisca_core.periods.typing import Period from .memory_usage import MemoryUsage @@ -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. @@ -211,6 +208,10 @@ def set_input( """ period = periods.build_period(period) + + if period is None: + raise ValueError(f"Invalid period value: {period}") + if period.unit == periods.ETERNITY and self.variable.definition_period != periods.ETERNITY: error_message = os.linesep.join([ 'Unable to set a value for variable {0} for periods.ETERNITY.', diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 5185fc9110..63421594be 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -27,8 +27,8 @@ """ -from ._config import INSTANT_PATTERN # noqa: F401 -from ._funcs import build_instant, build_period, key_period_size, parse_period # noqa: F401 -from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR # noqa: F401 -from .instant import Instant # noqa: F401 -from .period import Period # noqa: F401 +from ._config import INSTANT_PATTERN +from ._funcs import build_instant, build_period, key_period_size, parse_period +from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR +from .instant import Instant +from .period import Period diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index 3d3e06936b..45e6e41c17 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -2,9 +2,8 @@ from typing import Any -from openfisca_core import types - from ._units import DAY, MONTH, YEAR +from .typing import Instant LEARN_MORE = ( "Learn more about legal period formats in OpenFisca: " @@ -38,7 +37,7 @@ class InstantTypeError(TypeError): def __init__(self, value: Any) -> None: super().__init__( f"Invalid instant: {value} of type {type(value)}, expecting an " - f"{type(types.Instant)}. {LEARN_MORE}" + f"{type(Instant)}. {LEARN_MORE}" ) diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/_funcs.py index 99a3f6a2bf..4ba8452cd2 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/_funcs.py @@ -8,8 +8,6 @@ from pendulum.datetime import Date from pendulum.parsing import ParserError -from openfisca_core import types - from ._config import INSTANT_PATTERN from ._errors import InstantFormatError, PeriodFormatError from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR @@ -19,7 +17,7 @@ UNIT_MAPPING = {1: "year", 2: "month", 3: "day"} -def build_instant(value: Any) -> Optional[types.Instant]: +def build_instant(value: Any) -> Optional[Instant]: """Build a new instant, aka a triple of integers (year, month, day). Args: @@ -92,7 +90,7 @@ def build_instant(value: Any) -> Optional[types.Instant]: return Instant(instant) -def build_period(value: Any) -> types.Period: +def build_period(value: Any) -> Period: """Build a new period, aka a triple (unit, start_instant, size). Args: @@ -199,7 +197,7 @@ def build_period(value: Any) -> types.Period: return Period((unit, base_period.start, size)) -def key_period_size(period: types.Period) -> str: +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``. @@ -228,7 +226,7 @@ def key_period_size(period: types.Period) -> str: return f"{UNIT_WEIGHTS[unit]}_{size}" -def parse_period(value: str) -> Optional[types.Period]: +def parse_period(value: str) -> Optional[Period]: """Parse periods respecting the ISO format. Args: 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/population.py b/openfisca_core/populations/population.py index edf08044f0..76b4ecb516 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -10,7 +10,8 @@ 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 openfisca_core.periods.typing import Period +from openfisca_core.types import Array, Entity, Role, Simulation from . import config diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 35a90262d9..c8ec25f5aa 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -384,7 +384,7 @@ def _get_baseline_parameters_at_instant(self, instant): @functools.lru_cache() def get_parameters_at_instant( self, - instant: Union[str, int, types.Period, types.Instant], + instant: Union[str, int, Period, Instant], ) -> Optional[types.ParameterNodeAtInstant]: """Get the parameters of the legislation at a given instant @@ -396,13 +396,13 @@ def get_parameters_at_instant( """ - key: Optional[types.Instant] + key: Optional[Instant] msg: str - if isinstance(instant, types.Instant): + if isinstance(instant, Instant): key = instant - elif isinstance(instant, types.Period): + elif isinstance(instant, Period): key = instant.start elif isinstance(instant, (str, int)): 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..fdc1533dda 100644 --- a/openfisca_core/types/_data.py +++ b/openfisca_core/types/_data.py @@ -1,8 +1,8 @@ from typing import Sequence, TypeVar, Union -from nptyping import types, NDArray as Array - import numpy +from nptyping import NDArray as Array +from nptyping import types T = TypeVar("T", bool, bytes, float, int, object, str) diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types/_domain.py index 7eed350a78..276e3e6d7f 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_extensions import Protocol import abc +import numpy + class Entity(Protocol): """Entity protocol.""" @@ -37,7 +38,7 @@ class Formula(Protocol): def __call__( self, population: Population, - instant: Instant, + instant: Any, params: Params, ) -> numpy.ndarray: """Abstract method.""" @@ -55,20 +56,6 @@ def get_memory_usage(self) -> Any: """Abstract method.""" -@typing_extensions.runtime_checkable -class Instant(Protocol): - """Instant protocol.""" - - @property - @abc.abstractmethod - def date(self) -> Any: - """Abstract method.""" - - @abc.abstractmethod - def offset(self, offset: Any, unit: Any) -> Any: - """Abstract method.""" - - @typing_extensions.runtime_checkable class ParameterNodeAtInstant(Protocol): """ParameterNodeAtInstant protocol.""" @@ -78,7 +65,7 @@ class Params(Protocol): """Params protocol.""" @abc.abstractmethod - def __call__(self, instant: Instant) -> ParameterNodeAtInstant: + def __call__(self, instant: Any) -> ParameterNodeAtInstant: """Abstract method.""" diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index be2ea03aad..0fc14a58f6 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -14,7 +14,8 @@ 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 openfisca_core.periods.typing import Instant +from openfisca_core.types import Formula from . import config, helpers diff --git a/setup.cfg b/setup.cfg index 71851433ae..53fe85cbf1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,9 +12,10 @@ [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/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 @@ -48,10 +49,7 @@ 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.*] From 7a11ece47d6419f44f67bf4ecab164ddaba917bc Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 12:23:32 +0100 Subject: [PATCH 40/93] Simplify dict --- openfisca_core/periods/_units.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_units.py index fc243a6572..d3f6a04cfd 100644 --- a/openfisca_core/periods/_units.py +++ b/openfisca_core/periods/_units.py @@ -2,10 +2,4 @@ MONTH = "month" YEAR = "year" ETERNITY = "eternity" - -UNIT_WEIGHTS = { - DAY: 100, - MONTH: 200, - YEAR: 300, - ETERNITY: 400, - } +UNIT_WEIGHTS = {DAY: 100, MONTH: 200, YEAR: 300, ETERNITY: 400} From 69126fab17dd9509e840e7a16a28c8b1740884d8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 15:08:47 +0100 Subject: [PATCH 41/93] Remove assertion in periods.helpers --- openfisca_core/periods/__init__.py | 6 +++--- openfisca_core/periods/{_funcs.py => helpers.py} | 4 ++-- openfisca_core/periods/{instant.py => instant_.py} | 0 openfisca_core/periods/{period.py => period_.py} | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename openfisca_core/periods/{_funcs.py => helpers.py} (99%) rename openfisca_core/periods/{instant.py => instant_.py} (100%) rename openfisca_core/periods/{period.py => period_.py} (99%) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 63421594be..aa9f83a240 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -28,7 +28,7 @@ """ from ._config import INSTANT_PATTERN -from ._funcs import build_instant, build_period, key_period_size, parse_period from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR -from .instant import Instant -from .period import Period +from .helpers import build_instant, build_period, key_period_size, parse_period +from .instant_ import Instant +from .period_ import Period diff --git a/openfisca_core/periods/_funcs.py b/openfisca_core/periods/helpers.py similarity index 99% rename from openfisca_core/periods/_funcs.py rename to openfisca_core/periods/helpers.py index 4ba8452cd2..1c719dd2c6 100644 --- a/openfisca_core/periods/_funcs.py +++ b/openfisca_core/periods/helpers.py @@ -11,8 +11,8 @@ from ._config import INSTANT_PATTERN from ._errors import InstantFormatError, PeriodFormatError from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR -from .instant import Instant -from .period import Period +from .instant_ import Instant +from .period_ import Period UNIT_MAPPING = {1: "year", 2: "month", 3: "day"} diff --git a/openfisca_core/periods/instant.py b/openfisca_core/periods/instant_.py similarity index 100% rename from openfisca_core/periods/instant.py rename to openfisca_core/periods/instant_.py diff --git a/openfisca_core/periods/period.py b/openfisca_core/periods/period_.py similarity index 99% rename from openfisca_core/periods/period.py rename to openfisca_core/periods/period_.py index fe73c94e8c..2e0532db45 100644 --- a/openfisca_core/periods/period.py +++ b/openfisca_core/periods/period_.py @@ -7,7 +7,7 @@ from ._errors import DateUnitValueError from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR -from .instant import Instant +from .instant_ import Instant class Period(Tuple[str, Instant, int]): From 996d98763cc169db69e8d791baa8f2b395f11382 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 15:12:41 +0100 Subject: [PATCH 42/93] Avoid inf to respect period type --- openfisca_core/periods/_errors.py | 14 +++++++++++++- openfisca_core/periods/helpers.py | 16 +++++++--------- openfisca_core/periods/tests/test_funcs.py | 6 +++--- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index 45e6e41c17..f1ec62a44f 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -31,13 +31,25 @@ def __init__(self, value: Any) -> None: ) +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"{type(Instant)}. {LEARN_MORE}" + f"{type(Instant)}, {type(tuple)}, or {type(list)}. {LEARN_MORE}" ) diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 1c719dd2c6..1e9747e135 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -9,7 +9,7 @@ from pendulum.parsing import ParserError from ._config import INSTANT_PATTERN -from ._errors import InstantFormatError, PeriodFormatError +from ._errors import InstantFormatError, InstantValueError, PeriodFormatError from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR from .instant_ import Instant from .period_ import Period @@ -29,6 +29,7 @@ def build_instant(value: Any) -> Optional[Instant]: 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)) @@ -72,14 +73,11 @@ def build_instant(value: Any) -> Optional[Instant]: elif isinstance(value, int): instant = value, - elif isinstance(value, list): - assert 1 <= len(value) <= 3 - instant = tuple(value) + elif isinstance(value, (tuple, list)) and not 1 <= len(value) <= 3: + raise InstantValueError(value) else: - assert isinstance(value, tuple), value - assert 1 <= len(value) <= 3 - instant = value + instant = tuple(value) if len(instant) == 1: return Instant((instant[0], 1, 1)) @@ -110,7 +108,7 @@ def build_period(value: Any) -> Period: Period(('day', Instant((2021, 1, 1)), 1)) >>> build_period("eternity") - Period(('eternity', Instant((1, 1, 1)), inf)) + Period(('eternity', Instant((1, 1, 1)), 1)) >>> build_period(2021) Period(('year', Instant((2021, 1, 1)), 1)) @@ -142,7 +140,7 @@ def build_period(value: Any) -> Period: return Period((DAY, value, 1)) if value == "ETERNITY" or value == ETERNITY: - return Period(("eternity", build_instant(datetime.date.min), float("inf"))) + return Period(("eternity", build_instant(datetime.date.min), 1)) if isinstance(value, int): return Period((YEAR, Instant((value, 1, 1)), 1)) diff --git a/openfisca_core/periods/tests/test_funcs.py b/openfisca_core/periods/tests/test_funcs.py index b2fbd59cd8..7189c7c3ec 100644 --- a/openfisca_core/periods/tests/test_funcs.py +++ b/openfisca_core/periods/tests/test_funcs.py @@ -45,9 +45,9 @@ def test_build_instant_with_an_invalid_argument(arg, error): @pytest.mark.parametrize("arg, expected", [ - ["eternity", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), float("inf")))], - ["ETERNITY", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), float("inf")))], - [periods.ETERNITY, periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), float("inf")))], + ["eternity", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], + ["ETERNITY", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], + [periods.ETERNITY, periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], [periods.Instant((1, 1, 1)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 1))], [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365))], [1000, periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], From f34b6a651122285bb946be9e70e6baf323207b0f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 15:35:57 +0100 Subject: [PATCH 43/93] Refactor offset --- openfisca_core/periods/_errors.py | 5 +- openfisca_core/periods/instant_.py | 86 +++++--------------- openfisca_core/periods/period_.py | 4 +- openfisca_core/periods/tests/test_instant.py | 13 ++- 4 files changed, 37 insertions(+), 71 deletions(-) diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index f1ec62a44f..43fc9e3e1a 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -3,7 +3,6 @@ from typing import Any from ._units import DAY, MONTH, YEAR -from .typing import Instant LEARN_MORE = ( "Learn more about legal period formats in OpenFisca: " @@ -49,7 +48,7 @@ class InstantTypeError(TypeError): def __init__(self, value: Any) -> None: super().__init__( f"Invalid instant: {value} of type {type(value)}, expecting an " - f"{type(Instant)}, {type(tuple)}, or {type(list)}. {LEARN_MORE}" + f"'Instant', 'tuple', or 'list'. {LEARN_MORE}" ) @@ -70,5 +69,5 @@ class OffsetTypeError(TypeError): def __init__(self, value: Any) -> None: super().__init__( f"Invalid offset: {value} of type {type(value)}, expecting an " - f"{type(int)}. {LEARN_MORE}" + f"'int'. {LEARN_MORE}" ) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 1aacd46dcb..e3a1e2fd81 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -2,6 +2,7 @@ from typing import Dict, Optional, Tuple, Union +from dateutil import relativedelta import calendar import datetime @@ -216,76 +217,31 @@ def offset(self, offset: Union[str, int], unit: str) -> Instant: if unit not in (DAY, MONTH, YEAR): raise DateUnitValueError(unit) - if offset == 'first-of': - if unit == MONTH: - day = 1 + if offset == "first-of" and unit == YEAR: + return Instant((year, 1, 1)) - elif unit == YEAR: - month = 1 - day = 1 + if offset == "first-of" and unit == MONTH: + return Instant((year, month, 1)) - elif offset == 'last-of': - if unit == MONTH: - day = calendar.monthrange(year, month)[1] + if offset == "last-of" and unit == YEAR: + return Instant((year, 12, 31)) - elif unit == YEAR: - month = 12 - day = 31 + if offset == "last-of" and unit == MONTH: + return Instant((year, month, calendar.monthrange(year, month)[1])) - else: - if not isinstance(offset, int): - raise OffsetTypeError(offset) + if not isinstance(offset, int): + raise OffsetTypeError(offset) - if unit == DAY: - day += offset + if unit == YEAR: + date = self.date + relativedelta.relativedelta(years = offset) + return Instant((date.year, date.month, date.day)) - if offset < 0: - while day < 1: - month -= 1 + if unit == MONTH: + date = self.date + relativedelta.relativedelta(months = offset) + return Instant((date.year, date.month, date.day)) - if month == 0: - year -= 1 - month = 12 + if unit == DAY: + date = self.date + relativedelta.relativedelta(days = offset) + return Instant((date.year, date.month, date.day)) - 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 == 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 == 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 Instant((year, month, day)) + return self diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 2e0532db45..7aad8f05f0 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -536,4 +536,6 @@ def offset( """ - return Period((self[0], self[1].offset(offset, self[0] if unit is None else unit), self[2])) + start = self[1].offset(offset, self[0] if unit is None else unit) + + return Period((self[0], start, self[2])) diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 63265e2766..e4b76f4b6f 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -40,10 +40,8 @@ def test_to_date_with_an_invalid_argument(arg, error): @pytest.mark.parametrize("offset, unit, expected", [ ["first-of", periods.YEAR, periods.Instant((2020, 1, 1))], ["first-of", periods.MONTH, periods.Instant((2020, 2, 1))], - ["first-of", periods.DAY, periods.Instant((2020, 2, 29))], ["last-of", periods.YEAR, periods.Instant((2020, 12, 31))], ["last-of", periods.MONTH, periods.Instant((2020, 2, 29))], - ["last-of", periods.DAY, periods.Instant((2020, 2, 29))], [-3, periods.YEAR, periods.Instant((2017, 2, 28))], [-3, periods.MONTH, periods.Instant((2019, 11, 29))], [-3, periods.DAY, periods.Instant((2020, 2, 26))], @@ -54,3 +52,14 @@ def test_to_date_with_an_invalid_argument(arg, error): 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.DAY, TypeError], + ["last-of", periods.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) From 923c8a04eaf12461592b0ea16ea9d36bfbc28667 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 19:45:07 +0100 Subject: [PATCH 44/93] Deprecate instant to date --- CHANGELOG.md | 14 ++++-- openfisca_core/periods/instant_.py | 51 ++++---------------- openfisca_core/periods/tests/test_instant.py | 28 ----------- 3 files changed, 20 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09b367ff5d..b39a93830c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,20 +4,26 @@ #### Breaking changes +- Deprecate `instant_date`. - Deprecate `periods.intersect`. - Deprecate `periods.unit_weight`. - Deprecate `periods.unit_weights`. -- Rename `instant` to `build_instant`. - Rename `period` to `build_period`. -- Move `instant_date` to `Instant.to_date`. +- Rename `instant` to `build_instant`. - Refactor `Period.contains` as `Period.__contains__`. - Make `periods.parse_period` stricter (for example `2022-1` now fails). #### Technical changes -- Add typing to `openfisca_core.periods`. -- Fix `openfisca_core.periods` doctests. - Document `openfisca_core.periods`. +- Fix `openfisca_core.periods` doctests. +- Add typing to `openfisca_core.periods`. + +#### Bug fixes + +- Fixes incoherent dates, +- Fixes several race conditions, +- Fixes impossible `last-of` and `first-of` offsets. # 38.0.0 [#989](https://github.com/openfisca/openfisca-core/pull/989) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index e3a1e2fd81..700d5dd2b7 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,12 +1,12 @@ from __future__ import annotations -from typing import Dict, Optional, Tuple, Union +from typing import Dict, Tuple, Union from dateutil import relativedelta import calendar import datetime -from ._errors import DateUnitValueError, InstantTypeError, OffsetTypeError +from ._errors import DateUnitValueError, OffsetTypeError from ._units import DAY, MONTH, YEAR @@ -74,44 +74,14 @@ def __repr__(self) -> str: return f"{Instant.__name__}({super(Instant, self).__repr__()})" def __str__(self) -> str: - string = Instant._strings.get(self) + try: + return Instant._strings[self] - if string is not None: - return string - - Instant._strings = {self: self.date.isoformat(), **Instant._strings} + except KeyError: + Instant._strings = {self: self.date.isoformat(), **Instant._strings} return str(self) - @staticmethod - def to_date(value: Optional[Instant]) -> Optional[datetime.date]: - """Returns the date representation of an ``Instant``. - - Args: - value (Any): - An ``instant-like`` object to get the date from. - - Returns: - None: When ``value`` is None. - datetime.date: Otherwise. - - Raises: - InstantTypeError: When ``value`` is not an ``Instant``. - - Examples: - >>> Instant.to_date(Instant((2021, 1, 1))) - datetime.date(2021, 1, 1) - - """ - - if value is None: - return None - - if isinstance(value, Instant): - return value.date - - raise InstantTypeError(value) - @property def year(self) -> int: """The ``year`` of the ``Instant``. @@ -174,12 +144,11 @@ def date(self) -> datetime.date: """ - date = Instant._dates.get(self) - - if date is not None: - return date + try: + return Instant._dates[self] - Instant._dates = {self: datetime.date(*self), **Instant._dates} + except KeyError: + Instant._dates = {self: datetime.date(*self), **Instant._dates} return self.date diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index e4b76f4b6f..dcbeba6d8e 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -1,5 +1,3 @@ -import datetime - import pytest from openfisca_core import periods @@ -11,32 +9,6 @@ def instant(): return periods.Instant((2020, 2, 29)) -@pytest.mark.parametrize("arg, expected", [ - [None, None], - [periods.Instant((1, 1, 1)), datetime.date(1, 1, 1)], - [periods.Instant((4, 2, 29)), datetime.date(4, 2, 29)], - ]) -def test_to_date(arg, expected): - """Returns the expected ``date``.""" - assert periods.Instant.to_date(arg) == expected - - -@pytest.mark.parametrize("arg, error", [ - [(1, 1, 1), TypeError], - [periods.Instant((-1, 1, 1)), ValueError], - [periods.Instant((1, -1, 1)), ValueError], - [periods.Instant((1, 1, -1)), ValueError], - [periods.Instant((1, 13, 1)), ValueError], - [periods.Instant((1, 1, 32)), ValueError], - [periods.Instant((1, 2, 29)), ValueError], - ]) -def test_to_date_with_an_invalid_argument(arg, error): - """Raises ``ValueError`` when given an invalid argument.""" - - with pytest.raises(error): - periods.Instant.to_date(arg) - - @pytest.mark.parametrize("offset, unit, expected", [ ["first-of", periods.YEAR, periods.Instant((2020, 1, 1))], ["first-of", periods.MONTH, periods.Instant((2020, 2, 1))], From 66224e566dfe3d87978a4adeb232479432d1f243 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 14 Dec 2022 21:20:24 +0100 Subject: [PATCH 45/93] Rename to --- CHANGELOG.md | 13 +- openfisca_core/periods/instant_.py | 36 ++--- openfisca_core/periods/period_.py | 146 ++++++++++-------- openfisca_core/periods/tests/test_period.py | 2 +- .../scripts/measure_performances.py | 2 +- openfisca_core/simulations/simulation.py | 4 +- .../simulations/simulation_builder.py | 2 +- openfisca_core/variables/variable.py | 2 +- tests/core/variables/test_annualize.py | 6 +- 9 files changed, 109 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b39a93830c..84c278fc41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,22 +8,25 @@ - Deprecate `periods.intersect`. - Deprecate `periods.unit_weight`. - Deprecate `periods.unit_weights`. -- Rename `period` to `build_period`. -- Rename `instant` to `build_instant`. -- Refactor `Period.contains` as `Period.__contains__`. - 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. -- Add typing to `openfisca_core.periods`. #### Bug fixes +- Fixes impossible `last-of` and `first-of` offsets. - Fixes incoherent dates, - Fixes several race conditions, -- Fixes impossible `last-of` and `first-of` offsets. # 38.0.0 [#989](https://github.com/openfisca/openfisca-core/pull/989) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 700d5dd2b7..1d84292ce7 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import Dict, Tuple, Union +from typing import Tuple, Union from dateutil import relativedelta import calendar import datetime +import functools from ._errors import DateUnitValueError, OffsetTypeError from ._units import DAY, MONTH, YEAR @@ -64,23 +65,12 @@ class Instant(Tuple[int, int, int]): """ - #: A cache with the ``datetime.date`` representation of the ``Instant``. - _dates: Dict[Instant, datetime.date] = {} - - #: A cache with the ``str`` representation of the ``Instant``. - _strings: Dict[Instant, str] = {} - def __repr__(self) -> str: return f"{Instant.__name__}({super(Instant, self).__repr__()})" + @functools.lru_cache(maxsize = None) def __str__(self) -> str: - try: - return Instant._strings[self] - - except KeyError: - Instant._strings = {self: self.date.isoformat(), **Instant._strings} - - return str(self) + return self.date().isoformat() @property def year(self) -> int: @@ -130,13 +120,13 @@ def day(self) -> int: return self[2] - @property + @functools.lru_cache(maxsize = None) def date(self) -> datetime.date: """The date representation of the ``Instant``. Example: >>> instant = Instant((2021, 10, 1)) - >>> instant.date + >>> instant.date() datetime.date(2021, 10, 1) Returns: @@ -144,13 +134,7 @@ def date(self) -> datetime.date: """ - try: - return Instant._dates[self] - - except KeyError: - Instant._dates = {self: datetime.date(*self), **Instant._dates} - - return self.date + return datetime.date(*self) def offset(self, offset: Union[str, int], unit: str) -> Instant: """Increments/decrements the given instant with offset units. @@ -202,15 +186,15 @@ def offset(self, offset: Union[str, int], unit: str) -> Instant: raise OffsetTypeError(offset) if unit == YEAR: - date = self.date + relativedelta.relativedelta(years = offset) + date = self.date() + relativedelta.relativedelta(years = offset) return Instant((date.year, date.month, date.day)) if unit == MONTH: - date = self.date + relativedelta.relativedelta(months = offset) + date = self.date() + relativedelta.relativedelta(months = offset) return Instant((date.year, date.month, date.day)) if unit == DAY: - date = self.date + relativedelta.relativedelta(days = offset) + date = self.date() + relativedelta.relativedelta(days = offset) return Instant((date.year, date.month, date.day)) return self diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 7aad8f05f0..bd51fc3b09 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -124,24 +124,28 @@ def __str__(self) -> str: if month == 1: # civil year starting from january return str(year) + else: # rolling year - return f'{YEAR}:{year}-{month:02d}' + return f"{YEAR}:{year}-{month:02d}" + # simple month if unit == MONTH and size == 1: - return f'{year}-{month:02d}' + return f"{year}-{month:02d}" + # several civil years if unit == YEAR and month == 1: - return f'{unit}:{year}:{size}' + return f"{unit}:{year}:{size}" if unit == DAY: if size == 1: - return f'{year}-{month:02d}-{day:02d}' + return f"{year}-{month:02d}-{day:02d}" + else: - return f'{unit}:{year}-{month:02d}-{day:02d}:{size}' + return f"{unit}:{year}-{month:02d}-{day:02d}:{size}" # complex period - return f'{unit}:{year}-{month:02d}:{size}' + return f"{unit}:{year}-{month:02d}:{size}" def __contains__(self, other: object) -> bool: """Checks if a ``period`` contains another one. @@ -166,34 +170,6 @@ def __contains__(self, other: object) -> bool: return super().__contains__(other) - @property - 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((YEAR, instant, 1)) - >>> period.date - datetime.date(2021, 10, 1) - - >>> period = Period((YEAR, instant, 3)) - >>> period.date - Traceback (most recent call last): - ValueError: "date" is undefined for a period of size > 1: year:2021-10:3. - - """ - - if self.size != 1: - raise ValueError(f'"date" is undefined for a period of size > 1: {self}.') - - return self.start.date - @property def unit(self) -> str: """The ``unit`` of the ``Period``. @@ -230,7 +206,7 @@ def days(self) -> int: """ - return (self.stop.date - self.start.date).days + 1 + return (self.stop.date() - self.start.date()).days + 1 @property def size(self) -> int: @@ -309,7 +285,7 @@ def size_in_days(self) -> int: if unit in [MONTH, YEAR]: last_day = self.start.offset(length, unit).offset(-1, DAY) - return (last_day.date - self.start.date).days + 1 + return (last_day.date() - self.start.date()).days + 1 raise ValueError(f"Cannot calculate number of days in {unit}") @@ -337,6 +313,9 @@ def stop(self) -> Instant: Returns: An Instant. + Raises: + DateUnitValueError: If the period's unit isn't day, month or year. + Examples: >>> Period(("year", Instant((2012, 2, 29)), 1)).stop Instant((2013, 2, 28)) @@ -359,37 +338,49 @@ def stop(self) -> Instant: 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': + if unit == "month": month += size while month > 12: year += 1 month -= 12 else: - assert unit == 'year', f'Invalid unit: {unit} of type {type(unit)}' + if not unit == "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)) @@ -469,43 +460,32 @@ def first_day(self) -> Period: """ return Period((DAY, self.start, 1)) - def get_subperiods(self, unit: str) -> Sequence[Period]: - """Return the list of all the periods of unit ``unit``. - - Args: - unit: A string representing period's ``unit``. + def date(self) -> datetime.date: + """The date representation of the ``period``'s' start date. Returns: - A list of periods. + A datetime.date. Raises: - DateUnitValueError: If the ``unit`` is not a valid date unit. - ValueError: If the period's unit is smaller than the given unit. + ValueError: If the period's size is greater than 1. Examples: - >>> period = Period((YEAR, Instant((2021, 1, 1)), 1)) - >>> period.get_subperiods(MONTH) - [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] + >>> instant = Instant((2021, 10, 1)) + >>> period = Period((YEAR, instant, 1)) + >>> period.date() + datetime.date(2021, 10, 1) - >>> period = Period((YEAR, Instant((2021, 1, 1)), 2)) - >>> period.get_subperiods(YEAR) - [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] + >>> period = Period((YEAR, instant, 3)) + >>> period.date() + Traceback (most recent call last): + ValueError: 'date' undefined for period size > 1: year:2021-10:3. """ - if UNIT_WEIGHTS[self.unit] < UNIT_WEIGHTS[unit]: - raise ValueError(f"Cannot subdivide {self.unit} into {unit}") - - if unit == YEAR: - return [self.this_year.offset(i, YEAR) for i in range(self.size)] + if self.size > 1: + raise ValueError(f"'date' undefined for period size > 1: {self}.") - if unit == MONTH: - return [self.first_month.offset(i, MONTH) for i in range(self.size_in_months)] - - if unit == DAY: - return [self.first_day.offset(i, DAY) for i in range(self.size_in_days)] - - raise DateUnitValueError(unit) + return self.start.date() def offset( self, @@ -539,3 +519,41 @@ def offset( start = self[1].offset(offset, 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((YEAR, Instant((2021, 1, 1)), 1)) + >>> period.subperiods(MONTH) + [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] + + >>> period = Period((YEAR, Instant((2021, 1, 1)), 2)) + >>> period.subperiods(YEAR) + [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] + + """ + + if UNIT_WEIGHTS[self.unit] < UNIT_WEIGHTS[unit]: + raise ValueError(f"Cannot subdivide {self.unit} into {unit}") + + if unit == YEAR: + return [self.this_year.offset(i, YEAR) for i in range(self.size)] + + if unit == MONTH: + return [self.first_month.offset(i, MONTH) for i in range(self.size_in_months)] + + if unit == DAY: + return [self.first_day.offset(i, DAY) for i in range(self.size_in_days)] + + raise DateUnitValueError(unit) diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 5801740537..60c068c567 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -54,7 +54,7 @@ def test_subperiods(instant, period_unit, unit, start, cease, count): """Returns the expected subperiods.""" period = periods.Period((period_unit, instant, 3)) - subperiods = period.get_subperiods(unit) + subperiods = period.subperiods(unit) assert len(subperiods) == count assert subperiods[0] == periods.Period((unit, start, 1)) diff --git a/openfisca_core/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index 5e0e01a218..341e61e47e 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -106,7 +106,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): diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 2c207c93e2..a92686f984 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -185,7 +185,7 @@ def calculate_add(self, variable_name: str, 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): @@ -439,7 +439,7 @@ def set_input(self, variable_name: str, period, value): raise VariableNotFoundError(variable_name, self.tax_benefit_system) period = periods.build_period(period) - if ((variable.end is not None) and (period.start.date > variable.end)): + 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 71193296b3..615a6995a4 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -422,7 +422,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/variables/variable.py b/openfisca_core/variables/variable.py index 0fc14a58f6..32f14e05fa 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -348,7 +348,7 @@ def get_formula( 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/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index 8479c09357..c1cb1bbb3f 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -47,7 +47,7 @@ def test_without_annualize(monthly_variable): yearly_sum = sum( person('monthly_variable', month) - for month in period.get_subperiods(MONTH) + for month in period.subperiods(MONTH) ) assert monthly_variable.calculation_count == 11 @@ -62,7 +62,7 @@ def test_with_annualize(monthly_variable): yearly_sum = sum( person('monthly_variable', month) - for month in period.get_subperiods(MONTH) + for month in period.subperiods(MONTH) ) assert monthly_variable.calculation_count == 0 @@ -77,7 +77,7 @@ def test_with_partial_annualize(monthly_variable): yearly_sum = sum( person('monthly_variable', month) - for month in period.get_subperiods(MONTH) + for month in period.subperiods(MONTH) ) assert monthly_variable.calculation_count == 11 From 3b3c27a97966036b687a4d9074a9637e6b8427ae Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 15 Dec 2022 12:56:28 +0100 Subject: [PATCH 46/93] Fix typing --- openfisca_core/periods/helpers.py | 6 +++--- openfisca_core/periods/instant_.py | 4 ++-- openfisca_core/periods/period_.py | 6 +++--- openfisca_core/types/_domain.py | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 1e9747e135..82849e99b7 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any import datetime @@ -17,7 +17,7 @@ UNIT_MAPPING = {1: "year", 2: "month", 3: "day"} -def build_instant(value: Any) -> Optional[Instant]: +def build_instant(value: Any) -> Instant | None: """Build a new instant, aka a triple of integers (year, month, day). Args: @@ -224,7 +224,7 @@ def key_period_size(period: Period) -> str: return f"{UNIT_WEIGHTS[unit]}_{size}" -def parse_period(value: str) -> Optional[Period]: +def parse_period(value: str) -> Period | None: """Parse periods respecting the ISO format. Args: diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 1d84292ce7..8a37854c1e 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Tuple, Union +from typing import Tuple from dateutil import relativedelta import calendar @@ -136,7 +136,7 @@ def date(self) -> datetime.date: return datetime.date(*self) - def offset(self, offset: Union[str, int], unit: str) -> Instant: + def offset(self, offset: str | int, unit: str) -> Instant: """Increments/decrements the given instant with offset units. Args: diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index bd51fc3b09..ef4d5d29c4 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Optional, Sequence, Tuple, Union +from typing import Sequence, Tuple import calendar import datetime @@ -489,8 +489,8 @@ def date(self) -> datetime.date: def offset( self, - offset: Union[str, int], - unit: Optional[str] = None, + offset: str | int, + unit: str | None = None, ) -> Period: """Increment (or decrement) the given period with offset units. diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types/_domain.py index 276e3e6d7f..12574b3b9d 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types/_domain.py @@ -1,7 +1,7 @@ from __future__ import annotations import typing_extensions -from typing import Any, Optional +from typing import Any from typing_extensions import Protocol import abc @@ -27,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.""" @@ -130,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.""" @@ -143,7 +143,7 @@ class TaxBenefitSystem(Protocol): def get_variable( self, variable_name: Any, check_existence: Any = ..., - ) -> Optional[Any]: + ) -> Any | None: """Abstract method.""" From ed844c87313fdd62fd42d29bb804e3a2942e16e2 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 00:29:55 +0100 Subject: [PATCH 47/93] Add isort config --- setup.cfg | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/setup.cfg b/setup.cfg index 53fe85cbf1..9608c7cfe7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,6 +20,18 @@ rst-directives = attribute, deprecated, seealso, versionadded, versionchang 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 From 51d36998fcf21b86a0fb30f2b0f81baabccd6c22 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 00:30:54 +0100 Subject: [PATCH 48/93] Rename build_instant to Instant.build --- CHANGELOG.md | 2 +- openfisca_core/parameters/at_instant_like.py | 2 +- openfisca_core/periods/__init__.py | 2 +- openfisca_core/periods/helpers.py | 72 +---------------- openfisca_core/periods/instant_.py | 79 ++++++++++++++++++- openfisca_core/periods/tests/test_funcs.py | 4 +- .../taxbenefitsystems/tax_benefit_system.py | 2 +- openfisca_core/variables/variable.py | 2 +- 8 files changed, 86 insertions(+), 79 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84c278fc41..72cada064b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - 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 `instant` to `Instant.build`. - Rename `period` to `build_period`. - Transform `Instant.date` from property to method. - Transform `Period.date` from property to method. diff --git a/openfisca_core/parameters/at_instant_like.py b/openfisca_core/parameters/at_instant_like.py index 1b799b24ed..2626fcf067 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.build_instant(instant)) + instant = str(periods.Instant.build(instant)) return self._get_at_instant(instant) @abc.abstractmethod diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index aa9f83a240..fa73ca8f8d 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -29,6 +29,6 @@ from ._config import INSTANT_PATTERN from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR -from .helpers import build_instant, build_period, key_period_size, parse_period +from .helpers import build_period, key_period_size, parse_period from .instant_ import Instant from .period_ import Period diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 82849e99b7..3b28880f45 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -17,76 +17,6 @@ UNIT_MAPPING = {1: "year", 2: "month", 3: "day"} -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(("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)) 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). @@ -140,7 +70,7 @@ def build_period(value: Any) -> Period: return Period((DAY, value, 1)) if value == "ETERNITY" or value == ETERNITY: - return Period(("eternity", build_instant(datetime.date.min), 1)) + return Period(("eternity", Instant.build(datetime.date.min), 1)) if isinstance(value, int): return Period((YEAR, Instant((value, 1, 1)), 1)) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 8a37854c1e..510ffc6280 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -2,13 +2,19 @@ from typing import Tuple -from dateutil import relativedelta import calendar import datetime import functools +from dateutil import relativedelta + from ._errors import DateUnitValueError, OffsetTypeError from ._units import DAY, MONTH, YEAR +from .typing import Period + +from ._config import INSTANT_PATTERN + +from ._errors import InstantFormatError, InstantValueError class Instant(Tuple[int, int, int]): @@ -198,3 +204,74 @@ def offset(self, offset: str | int, unit: str) -> Instant: return Instant((date.year, date.month, date.day)) return self + + @staticmethod + def build(value: Any) -> Instant: + """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(("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)) 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) diff --git a/openfisca_core/periods/tests/test_funcs.py b/openfisca_core/periods/tests/test_funcs.py index 7189c7c3ec..06634c2ee7 100644 --- a/openfisca_core/periods/tests/test_funcs.py +++ b/openfisca_core/periods/tests/test_funcs.py @@ -17,7 +17,7 @@ ]) def test_build_instant(arg, expected): """Returns the expected ``Instant``.""" - assert periods.build_instant(arg) == expected + assert periods.Instant.build(arg) == expected @pytest.mark.parametrize("arg, error", [ @@ -41,7 +41,7 @@ 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) + periods.Instant.build(arg) @pytest.mark.parametrize("arg, expected", [ diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index c8ec25f5aa..d3aa4e1e05 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -406,7 +406,7 @@ def get_parameters_at_instant( key = instant.start elif isinstance(instant, (str, int)): - key = periods.build_instant(instant) + key = periods.Instant.build(instant) else: msg = f"Expected an Instant (e.g. Instant((2017, 1, 1)) ). Got: {instant}." diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 32f14e05fa..ac52329ca2 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -343,7 +343,7 @@ def get_formula( instant = periods.build_period(period).start except ValueError: - instant = periods.build_instant(period) + instant = periods.Instant.build(period) if instant is None: return None From 885985d83a1a2490231920a07f21cac40ce1ac0d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 00:44:32 +0100 Subject: [PATCH 49/93] Fix doctests in Instant --- openfisca_core/periods/instant_.py | 41 +++++++++++++------- openfisca_core/periods/tests/test_funcs.py | 39 ------------------- openfisca_core/periods/tests/test_instant.py | 41 ++++++++++++++++++++ 3 files changed, 68 insertions(+), 53 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 510ffc6280..0ce1135a1e 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Tuple +from typing import Any, Tuple import calendar import datetime @@ -8,14 +8,16 @@ from dateutil import relativedelta -from ._errors import DateUnitValueError, OffsetTypeError +from ._config import INSTANT_PATTERN +from ._errors import ( + DateUnitValueError, + InstantFormatError, + InstantValueError, + OffsetTypeError, + ) from ._units import DAY, MONTH, YEAR from .typing import Period -from ._config import INSTANT_PATTERN - -from ._errors import InstantFormatError, InstantValueError - class Instant(Tuple[int, int, int]): """An instant in time (``year``, ``month``, ``day``). @@ -221,24 +223,29 @@ def build(value: Any) -> Instant: InstantValueError: When the length is out of range. Examples: - >>> build_instant(datetime.date(2021, 9, 16)) - Instant((2021, 9, 16)) + >>> from openfisca_core import periods - >>> build_instant(Instant((2021, 9, 16))) + >>> periods.Instant.build(datetime.date(2021, 9, 16)) Instant((2021, 9, 16)) - >>> build_instant(Period(("year", Instant((2021, 9, 16)), 1))) + >>> periods.Instant.build(Instant((2021, 9, 16))) Instant((2021, 9, 16)) - >>> build_instant("2021") + >>> periods.Instant.build("2021") Instant((2021, 1, 1)) - >>> build_instant(2021) + >>> periods.Instant.build(2021) Instant((2021, 1, 1)) - >>> build_instant((2021, 9)) + >>> periods.Instant.build((2021, 9)) Instant((2021, 9, 1)) + >>> start = periods.Instant((2021, 9, 16)) + >>> period = periods.Period(("year", start, 1)) + >>> periods.Instant.build(period) + Instant((2021, 9, 16)) + + .. versionadded:: 39.0.0 """ if value is None: @@ -254,7 +261,10 @@ def build(value: Any) -> Instant: raise InstantFormatError(value) if isinstance(value, str): - instant = tuple(int(fragment) for fragment in value.split('-', 2)[:3]) + instant = tuple( + int(fragment) + for fragment in value.split('-', 2)[:3] + ) elif isinstance(value, datetime.date): instant = value.year, value.month, value.day @@ -262,6 +272,9 @@ def build(value: Any) -> Instant: elif isinstance(value, int): instant = value, + elif isinstance(value, (dict, set)): + raise InstantValueError(value) + elif isinstance(value, (tuple, list)) and not 1 <= len(value) <= 3: raise InstantValueError(value) diff --git a/openfisca_core/periods/tests/test_funcs.py b/openfisca_core/periods/tests/test_funcs.py index 06634c2ee7..5ae894f973 100644 --- a/openfisca_core/periods/tests/test_funcs.py +++ b/openfisca_core/periods/tests/test_funcs.py @@ -5,45 +5,6 @@ from openfisca_core import periods -@pytest.mark.parametrize("arg, expected", [ - [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.DAY, periods.Instant((1, 1, 1)), 365)), periods.Instant((1, 1, 1))], - [1000, periods.Instant((1000, 1, 1))], - ["1000", periods.Instant((1000, 1, 1))], - ["1000-01", periods.Instant((1000, 1, 1))], - ["1000-01-01", periods.Instant((1000, 1, 1))], - ]) -def test_build_instant(arg, expected): - """Returns the expected ``Instant``.""" - assert periods.Instant.build(arg) == expected - - -@pytest.mark.parametrize("arg, error", [ - [periods.YEAR, ValueError], - [periods.ETERNITY, ValueError], - ["1000-0", ValueError], - ["1000-1", ValueError], - ["1000-13", ValueError], - ["1000-0-0", ValueError], - ["1000-1-1", ValueError], - ["1000-01-0", ValueError], - ["1000-01-1", ValueError], - ["1000-01-32", ValueError], - ["month:1000", ValueError], - ["month:1000:1", ValueError], - ["year:1000-01-01", ValueError], - ["year:1000-01-01:1", ValueError], - ["year:1000-01-01:3", ValueError], - ]) -def test_build_instant_with_an_invalid_argument(arg, error): - """Raises ``ValueError`` when given an invalid argument.""" - - with pytest.raises(error): - periods.Instant.build(arg) - - @pytest.mark.parametrize("arg, expected", [ ["eternity", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], ["ETERNITY", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index dcbeba6d8e..44d953732d 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -1,3 +1,5 @@ +import datetime + import pytest from openfisca_core import periods @@ -35,3 +37,42 @@ def test_offset_with_an_invalid_offset(instant, offset, unit, expected): with pytest.raises(TypeError): instant.offset(offset, unit) + + +@pytest.mark.parametrize("arg, expected", [ + [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.DAY, periods.Instant((1, 1, 1)), 365)), periods.Instant((1, 1, 1))], + [1000, periods.Instant((1000, 1, 1))], + ["1000", periods.Instant((1000, 1, 1))], + ["1000-01", periods.Instant((1000, 1, 1))], + ["1000-01-01", periods.Instant((1000, 1, 1))], + ]) +def test_build_instant(arg, expected): + """Returns the expected ``Instant``.""" + assert periods.Instant.build(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + [periods.YEAR, ValueError], + [periods.ETERNITY, ValueError], + ["1000-0", ValueError], + ["1000-1", ValueError], + ["1000-13", ValueError], + ["1000-0-0", ValueError], + ["1000-1-1", ValueError], + ["1000-01-0", ValueError], + ["1000-01-1", ValueError], + ["1000-01-32", ValueError], + ["month:1000", ValueError], + ["month:1000:1", ValueError], + ["year:1000-01-01", ValueError], + ["year:1000-01-01:1", ValueError], + ["year:1000-01-01:3", ValueError], + ]) +def test_build_instant_with_an_invalid_argument(arg, error): + """Raises ``ValueError`` when given an invalid argument.""" + + with pytest.raises(error): + periods.Instant.build(arg) From 3bcaf719c4ec0f526fd04800e7c8bb94e992cb77 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 05:57:20 +0100 Subject: [PATCH 50/93] Refactor the Period.stop function --- openfisca_core/periods/helpers.py | 4 +- openfisca_core/periods/instant_.py | 60 +- openfisca_core/periods/period_.py | 538 +++++++++++------- openfisca_core/periods/tests/test_funcs.py | 61 +- openfisca_core/periods/tests/test_instant.py | 35 +- openfisca_core/periods/tests/test_period.py | 72 +-- openfisca_core/periods/typing.py | 20 +- openfisca_core/simulations/simulation.py | 6 +- .../taxbenefitsystems/tax_benefit_system.py | 14 +- openfisca_core/variables/helpers.py | 2 +- setup.py | 1 + tests/core/test_countries.py | 4 +- 12 files changed, 492 insertions(+), 325 deletions(-) diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 3b28880f45..06256c66a4 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -8,8 +8,7 @@ from pendulum.datetime import Date from pendulum.parsing import ParserError -from ._config import INSTANT_PATTERN -from ._errors import InstantFormatError, InstantValueError, PeriodFormatError +from ._errors import PeriodFormatError from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR from .instant_ import Instant from .period_ import Period @@ -17,7 +16,6 @@ UNIT_MAPPING = {1: "year", 2: "month", 3: "day"} - def build_period(value: Any) -> Period: """Build a new period, aka a triple (unit, start_instant, size). diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 0ce1135a1e..86331ee990 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -8,11 +8,14 @@ from dateutil import relativedelta +import pendulum + from ._config import INSTANT_PATTERN from ._errors import ( DateUnitValueError, InstantFormatError, InstantValueError, + InstantTypeError, OffsetTypeError, ) from ._units import DAY, MONTH, YEAR @@ -74,7 +77,10 @@ class Instant(Tuple[int, int, int]): """ def __repr__(self) -> str: - return f"{Instant.__name__}({super(Instant, self).__repr__()})" + return ( + f"{type(self).__name__}" + f"({super(type(self), self).__repr__()})" + ) @functools.lru_cache(maxsize = None) def __str__(self) -> str: @@ -142,7 +148,7 @@ def date(self) -> datetime.date: """ - return datetime.date(*self) + return pendulum.date(*self) def offset(self, offset: str | int, unit: str) -> Instant: """Increments/decrements the given instant with offset units. @@ -178,49 +184,53 @@ def offset(self, offset: str | int, unit: str) -> Instant: if unit not in (DAY, MONTH, YEAR): raise DateUnitValueError(unit) - if offset == "first-of" and unit == YEAR: - return Instant((year, 1, 1)) + if offset in ("first-of", "last-of") and unit == DAY: + return self if offset == "first-of" and unit == MONTH: - return Instant((year, month, 1)) + return type(self)((year, month, 1)) - if offset == "last-of" and unit == YEAR: - return Instant((year, 12, 31)) + if offset == "first-of" and unit == YEAR: + return type(self)((year, 1, 1)) if offset == "last-of" and unit == MONTH: - return Instant((year, month, calendar.monthrange(year, month)[1])) + day = calendar.monthrange(year, month)[1] + return type(self)((year, month, day)) + + if offset == "last-of" and unit == YEAR: + return type(self)((year, 12, 31)) if not isinstance(offset, int): raise OffsetTypeError(offset) - if unit == YEAR: - date = self.date() + relativedelta.relativedelta(years = offset) - return Instant((date.year, date.month, date.day)) + if unit == DAY: + date = self.date() + relativedelta.relativedelta(days = offset) + return type(self)((date.year, date.month, date.day)) if unit == MONTH: date = self.date() + relativedelta.relativedelta(months = offset) - return Instant((date.year, date.month, date.day)) + return type(self)((date.year, date.month, date.day)) - if unit == DAY: - date = self.date() + relativedelta.relativedelta(days = offset) - return Instant((date.year, date.month, date.day)) + if unit == YEAR: + date = self.date() + relativedelta.relativedelta(years = offset) + return type(self)((date.year, date.month, date.day)) return self - @staticmethod - def build(value: Any) -> Instant: + @classmethod + def build(cls, value: Any) -> Instant: """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. + An Instant. Raises: - InstantFormatError: When the arguments were invalid, like "2021-32-13". - InstantValueError: When the length is out of range. + InstantFormatError: When ``value`` is invalid, like "2021-32-13". + InstantValueError: When the length of ``value`` is out of range. + InstantTypeError: When ``value`` is None. Examples: >>> from openfisca_core import periods @@ -249,7 +259,7 @@ def build(value: Any) -> Instant: """ if value is None: - return None + raise InstantTypeError(value) if isinstance(value, Instant): return value @@ -282,9 +292,9 @@ def build(value: Any) -> Instant: instant = tuple(value) if len(instant) == 1: - return Instant((instant[0], 1, 1)) + return cls((instant[0], 1, 1)) if len(instant) == 2: - return Instant((instant[0], instant[1], 1)) + return cls((instant[0], instant[1], 1)) - return Instant(instant) + return cls(instant) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index ef4d5d29c4..2eb5c3d8f1 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,13 +1,13 @@ from __future__ import annotations -from typing import Sequence, Tuple +from typing import Callable, Sequence, Tuple -import calendar -import datetime +import inflect +from pendulum import datetime from ._errors import DateUnitValueError from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR -from .instant_ import Instant +from .typing import Instant class Period(Tuple[str, Instant, int]): @@ -28,8 +28,10 @@ class Period(Tuple[str, Instant, int]): The ``unit``, ``start``, and ``size``, accordingly. Examples: - >>> instant = Instant((2021, 9, 1)) - >>> period = Period((YEAR, instant, 3)) + >>> from openfisca_core import periods + + >>> start = periods.Instant((2021, 9, 1)) + >>> period = Period((YEAR, start, 3)) ``Periods`` are represented as a ``tuple`` containing the ``unit``, an ``Instant`` and the ``size``: @@ -61,10 +63,10 @@ class Period(Tuple[str, Instant, int]): >>> len(period) 3 - >>> period == Period(("year", instant, 3)) + >>> period == Period((YEAR, start, 3)) True - >>> period > Period(("year", instant, 3)) + >>> period > Period((YEAR, start, 3)) False >>> unit, (year, month, day), size = period @@ -73,8 +75,13 @@ class Period(Tuple[str, Instant, int]): """ + plural: Callable[[str], str] = inflect.engine().plural + def __repr__(self) -> str: - return f"{Period.__name__}({super(Period, self).__repr__()})" + return ( + f"{type(self).__name__}" + f"({super(type(self), self).__repr__()})" + ) def __str__(self) -> str: """Transform period to a string. @@ -83,41 +90,37 @@ def __str__(self) -> str: str: A string representation of the period. Examples: - >>> str(Period(("year", Instant((2021, 1, 1)), 1))) + >>> from openfisca_core import periods + + >>> jan = periods.Instant((2021, 1, 1)) + >>> feb = jan.offset(1, MONTH) + + >>> str(Period((YEAR, jan, 1))) '2021' - >>> str(Period(("year", Instant((2021, 2, 1)), 1))) + >>> str(Period((YEAR, feb, 1))) 'year:2021-02' - >>> str(Period(("month", Instant((2021, 2, 1)), 1))) + >>> str(Period((MONTH, feb, 1))) '2021-02' - >>> str(Period(("year", Instant((2021, 1, 1)), 2))) + >>> str(Period((YEAR, jan, 2))) 'year:2021:2' - >>> str(Period(("month", Instant((2021, 1, 1)), 2))) + >>> str(Period((MONTH, jan, 2))) 'month:2021-01:2' - >>> str(Period(("month", Instant((2021, 1, 1)), 12))) + >>> str(Period((MONTH, jan, 12))) '2021' - >>> str(Period(("year", Instant((2021, 3, 1)), 2))) - 'year:2021-03:2' - - >>> str(Period(("month", Instant((2021, 3, 1)), 2))) - 'month:2021-03:2' - - >>> str(Period(("month", Instant((2021, 3, 1)), 12))) - 'year:2021-03' - """ - unit, start_instant, size = self + unit, start, size = self if unit == ETERNITY: return "ETERNITY" - year, month, day = start_instant + year, month, day = start # 1 year long period if unit == MONTH and size == 12 or unit == YEAR and size == 1: @@ -157,9 +160,11 @@ def __contains__(self, other: object) -> bool: True if ``other`` is contained, otherwise False. Example: - >>> period = Period((YEAR, Instant((2021, 1, 1)), 1)) - >>> sub_period = Period((MONTH, Instant((2021, 1, 1)), 3)) + >>> from openfisca_core import periods + >>> start = periods.Instant((2021, 1, 1)) + >>> period = Period((YEAR, start, 1)) + >>> sub_period = Period((MONTH, start, 3)) >>> sub_period in period True @@ -178,8 +183,10 @@ def unit(self) -> str: An int. Example: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((YEAR, instant, 3)) + >>> from openfisca_core import periods + + >>> start = periods.Instant((2021, 10, 1)) + >>> period = Period((YEAR, start, 3)) >>> period.unit 'year' @@ -188,25 +195,23 @@ def unit(self) -> str: return self[0] @property - def days(self) -> int: - """Count the number of days in period. + def start(self) -> Instant: + """The ``Instant`` at which the ``Period`` starts. Returns: - An int. + An Instant. - Examples: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((YEAR, instant, 3)) - >>> period.size_in_days - 1096 + Example: + >>> from openfisca_core import periods - >>> period = Period((MONTH, instant, 3)) - >>> period.size_in_days - 92 + >>> start = periods.Instant((2021, 10, 1)) + >>> period = Period((YEAR, start, 3)) + >>> period.start + Instant((2021, 10, 1)) """ - return (self.stop.date() - self.start.date()).days + 1 + return self[1] @property def size(self) -> int: @@ -216,8 +221,10 @@ def size(self) -> int: An int. Example: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((YEAR, instant, 3)) + >>> from openfisca_core import periods + + >>> start = periods.Instant((2021, 10, 1)) + >>> period = Period((YEAR, start, 3)) >>> period.size 3 @@ -225,6 +232,39 @@ def size(self) -> int: return self[2] + @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: + >>> from openfisca_core import periods + + >>> start = periods.Instant((2021, 10, 1)) + + >>> period = Period((YEAR, start, 3)) + >>> period.size_in_days + 1096 + + >>> period = Period((MONTH, start, 3)) + >>> period.size_in_days + 92 + + """ + + if self.unit == DAY: + return self.size + + if self.unit in (MONTH, YEAR): + return (self.stop.date() - self.start.date()).days + 1 + + raise ValueError(f"Cannot calculate number of days in {self.unit}.") + @property def size_in_months(self) -> int: """The ``size`` of the ``Period`` in months. @@ -236,75 +276,77 @@ def size_in_months(self) -> int: ValueError: If the period's unit is not a month or a year. Examples: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((YEAR, instant, 3)) + >>> from openfisca_core import periods + + >>> start = periods.Instant((2021, 10, 1)) + + >>> period = Period((YEAR, start, 3)) >>> period.size_in_months 36 - >>> period = Period((DAY, instant, 3)) + >>> period = Period((DAY, start, 3)) >>> period.size_in_months Traceback (most recent call last): ValueError: Cannot calculate number of months in day. """ - if self[0] == MONTH: - return self[2] + if self.unit == MONTH: + return self.size - if self[0] == YEAR: - return self[2] * 12 + if self.unit == YEAR: + return self.size * 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. + def size_in_years(self) -> int: + """The ``size`` of the ``Period`` in years. Examples: - >>> instant = Instant((2019, 10, 1)) - >>> period = Period((YEAR, instant, 3)) - >>> period.size_in_days - 1096 + >>> from openfisca_core import periods - >>> period = Period((MONTH, instant, 3)) - >>> period.size_in_days - 92 + >>> start = periods.Instant((2021, 10, 1)) - """ + >>> period = Period((YEAR, start, 3)) + >>> period.size_in_years + 3 - unit, instant, length = self + >>> period = Period((MONTH, start, 3)) + >>> period.size_in_years + Traceback (most recent call last): + ValueError: Cannot calculate number of years in month. - if unit == DAY: - return length + """ - if unit in [MONTH, YEAR]: - last_day = self.start.offset(length, unit).offset(-1, DAY) - return (last_day.date() - self.start.date()).days + 1 + if self.unit == YEAR: + return self.size - raise ValueError(f"Cannot calculate number of days in {unit}") + raise ValueError(f"Cannot calculate number of years in {self.unit}.") @property - def start(self) -> Instant: - """The ``Instant`` at which the ``Period`` starts. + def days(self) -> int: + """Count the number of days in period. Returns: - An Instant. + An int. - Example: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((YEAR, instant, 3)) - >>> period.start - Instant((2021, 10, 1)) + Examples: + >>> from openfisca_core import periods + + >>> start = periods.Instant((2021, 10, 1)) + + >>> period = Period((YEAR, start, 3)) + >>> period.days + 1096 + + >>> period = Period((MONTH, start, 3)) + >>> period.days + 92 """ - return self[1] + return self.size_in_days @property def stop(self) -> Instant: @@ -317,181 +359,249 @@ def stop(self) -> Instant: DateUnitValueError: If the period's unit isn't day, month or year. Examples: - >>> Period(("year", Instant((2012, 2, 29)), 1)).stop - Instant((2013, 2, 28)) + >>> from openfisca_core import periods + + >>> start = periods.Instant((2012, 2, 29)) - >>> Period(("month", Instant((2012, 2, 29)), 1)).stop - Instant((2012, 3, 28)) + >>> Period((YEAR, start, 2)).stop + Instant((2014, 2, 27)) - >>> Period(("day", Instant((2012, 2, 29)), 1)).stop - Instant((2012, 2, 29)) + >>> Period((MONTH, start, 36)).stop + Instant((2015, 2, 27)) + + >>> Period((DAY, start, 1096)).stop + Instant((2015, 2, 28)) """ - unit, start_instant, size = self - year, month, day = start_instant + unit, start, size = self if unit == ETERNITY: - return Instant((1, 1, 1)) + return type(self.start)((1, 1, 1)) - if unit == 'day': - if size > 1: - day += size - 1 - month_last_day = calendar.monthrange(year, month)[1] + stop = ( + start + .date() + .add(**{self.plural(unit): size}) + .subtract(days = 1) + ) - while day > month_last_day: - month += 1 + return type(start)((stop.year, stop.month, stop.day)) - if month == 13: - year += 1 - month = 1 + @property + def today(self) -> Period: + """A new day ``Period`` representing today. - day -= month_last_day - month_last_day = calendar.monthrange(year, month)[1] + Returns: + A Period. - else: - if unit == "month": - month += size - while month > 12: - year += 1 - month -= 12 - else: - if not unit == "year": - raise DateUnitValueError(unit) + Examples: + >>> from openfisca_core import periods - year += size - day -= 1 + >>> start = periods.Instant((2023, 1, 1)) - if day < 1: - month -= 1 + >>> period = Period((YEAR, start, 3)) - if month == 0: - year -= 1 - month = 12 + >>> period.today + Period(('day', Instant((2023, 1, 1)), 1)) - day += calendar.monthrange(year, month)[1] + .. versionadded:: 39.0.0 - else: - month_last_day = calendar.monthrange(year, month)[1] + """ + + return self.this(DAY) + + def date(self) -> datetime.Date: + """The date representation of the ``period``'s' start date. + + Returns: + A datetime.date. - if day > month_last_day: - month += 1 + Raises: + ValueError: If the period's size is greater than 1. - if month == 13: - year += 1 - month = 1 + Examples: + >>> from openfisca_core import periods - day -= month_last_day + >>> start = periods.Instant((2021, 10, 1)) - return Instant((year, month, day)) + >>> period = Period((YEAR, start, 1)) + >>> period.date() + Date(2021, 10, 1) - @property - def last_month(self) -> Period: - """Last month of the ``Period``. + >>> period = Period((YEAR, start, 3)) + >>> period.date() + Traceback (most recent call last): + ValueError: 'date' undefined for period size > 1: year:2021-10:3. - Returns: - A Period. + .. vesionchanged:: 39.0.0: + Made it a normal method instead of a property. """ - return self.first_month.offset(-1) + if self.size > 1: + raise ValueError(f"'date' undefined for period size > 1: {self}.") - @property - def last_3_months(self) -> Period: - """Last 3 months of the ``Period``. + return self.start.date() + + def size_in(self, unit: str) -> int: + """The ``size`` of the ``Period`` in the given unit. + + Args: + unit: The unit to convert to. Returns: - A Period. + An int. - """ + Raises: + ValueError: If the period's unit is not a day, a month or a year. - start: Instant = self.first_month.start - return Period((MONTH, start, 3)).offset(-3) + Examples: + >>> from openfisca_core import periods - @property - def last_year(self) -> Period: - """Last year of the ``Period``.""" - start: Instant = self.start.offset("first-of", YEAR) - return Period((YEAR, start, 1)).offset(-1) + >>> start = periods.Instant((2022, 1, 1)) - @property - def n_2(self) -> Period: - """Last 2 years of the ``Period``. + >>> period = Period((YEAR, start, 3)) - Returns: - A Period. + >>> period.size_in(DAY) + 1096 + + >>> period.size_in(MONTH) + 36 + + >>> period.size_in(YEAR) + 3 + + .. versionadded:: 39.0.0 """ - start: Instant = self.start.offset("first-of", YEAR) - return Period((YEAR, start, 1)).offset(-2) + if unit == DAY: + return self.size_in_days + + if unit == MONTH: + return self.size_in_months - @property - def this_year(self) -> Period: - """A new year ``Period`` starting at the beginning of the year. + if unit == YEAR: + return self.size_in_years + + raise ValueError(f"Cannot calculate number of {unit} in {self.unit}.") + + def this(self, unit: str) -> Period: + """A new month ``Period`` starting at the first of ``unit``. + + Args: + unit: The unit of the requested Period. Returns: A Period. + Examples: + >>> from openfisca_core import periods + + >>> start = periods.Instant((2023, 1, 1)) + + >>> period = Period((YEAR, start, 3)) + + >>> period.this(DAY) + Period(('day', Instant((2023, 1, 1)), 1)) + + >>> period.this(MONTH) + Period(('month', Instant((2023, 1, 1)), 1)) + + >>> period.this(YEAR) + Period(('year', Instant((2023, 1, 1)), 1)) + + .. versionadded:: 39.0.0 + """ - start: Instant = self.start.offset("first-of", YEAR) - return Period((YEAR, start, 1)) + return type(self)((unit, self.start.offset("first-of", unit), 1)) - @property - def first_month(self) -> Period: - """A new month ``Period`` starting at the first of the month. + + def last(self, unit: str, size: int = 1) -> Period: + """Last ``size`` ``unit``s of the ``Period``. + + Args: + unit: The unit of the requested Period. + size: The number of units to include in the Period. Returns: A Period. + Examples: + >>> from openfisca_core import periods + + >>> start = periods.Instant((2023, 1, 1)) + + >>> period = Period((YEAR, start, 3)) + + >>> period.last(DAY) + Period(('day', Instant((2022, 12, 31)), 1)) + + >>> period.last(DAY, 7) + Period(('day', Instant((2022, 12, 25)), 7)) + + >>> period.last(MONTH) + Period(('month', Instant((2022, 12, 1)), 1)) + + >>> period.last(MONTH, 3) + Period(('month', Instant((2022, 10, 1)), 3)) + + >>> period.last(YEAR) + Period(('year', Instant((2022, 1, 1)), 1)) + + >>> period.last(YEAR, 1) + Period(('year', Instant((2022, 1, 1)), 1)) + + .. versionadded:: 39.0.0 + """ - start: Instant = self.start.offset("first-of", MONTH) - return Period((MONTH, start, 1)) + return type(self)((unit, self.ago(unit, size).start, size)) - @property - def first_day(self) -> Period: - """A new day ``Period``. + def ago(self, unit: str, size: int = 1) -> Period: + """``size`` ``unit``s ago of the ``Period``. + + Args: + unit: The unit of the requested Period. + size: The number of units ago. Returns: A Period. - """ - return Period((DAY, self.start, 1)) + Examples: + >>> from openfisca_core import periods - def date(self) -> datetime.date: - """The date representation of the ``period``'s' start date. + >>> start = periods.Instant((2023, 1, 1)) - Returns: - A datetime.date. + >>> period = Period((YEAR, start, 3)) - Raises: - ValueError: If the period's size is greater than 1. + >>> period.ago(DAY) + Period(('day', Instant((2022, 12, 31)), 1)) - Examples: - >>> instant = Instant((2021, 10, 1)) - >>> period = Period((YEAR, instant, 1)) - >>> period.date() - datetime.date(2021, 10, 1) + >>> period.ago(DAY, 7) + Period(('day', Instant((2022, 12, 25)), 1)) - >>> period = Period((YEAR, instant, 3)) - >>> period.date() - Traceback (most recent call last): - ValueError: 'date' undefined for period size > 1: year:2021-10:3. + >>> period.ago(MONTH) + Period(('month', Instant((2022, 12, 1)), 1)) - """ + >>> period.ago(MONTH, 3) + Period(('month', Instant((2022, 10, 1)), 1)) - if self.size > 1: - raise ValueError(f"'date' undefined for period size > 1: {self}.") + >>> period.ago(YEAR) + Period(('year', Instant((2022, 1, 1)), 1)) - return self.start.date() + >>> period.ago(YEAR, 1) + Period(('year', Instant((2022, 1, 1)), 1)) + + .. versionadded:: 39.0.0 + + """ + + return type(self)((unit, self.this(unit).start, 1)).offset(-size) - def offset( - self, - offset: str | int, - unit: str | None = None, - ) -> Period: + def offset(self, offset: str | int, unit: str | None = None) -> Period: """Increment (or decrement) the given period with offset units. Args: @@ -502,23 +612,29 @@ def offset( Period: A new one. Examples: - >>> Period(("day", Instant((2014, 2, 3)), 1)).offset("first-of", "month") + >>> from openfisca_core import periods + + >>> start = periods.Instant((2014, 2, 3)) + + >>> Period((DAY, start, 1)).offset("first-of", MONTH) Period(('day', Instant((2014, 2, 1)), 1)) - >>> Period(("month", Instant((2014, 2, 3)), 4)).offset("last-of", "month") + >>> Period((MONTH, start, 4)).offset("last-of", MONTH) Period(('month', Instant((2014, 2, 28)), 4)) - >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(-3) + >>> start = periods.Instant((2021, 1, 1)) + + >>> Period((DAY, start, 365)).offset(-3) Period(('day', Instant((2020, 12, 29)), 365)) - >>> Period(("day", Instant((2021, 1, 1)), 365)).offset(1, "year") + >>> Period((DAY, start, 365)).offset(1, YEAR) Period(('day', Instant((2022, 1, 1)), 365)) """ start = self[1].offset(offset, self[0] if unit is None else unit) - return Period((self[0], start, self[2])) + return type(self)((self[0], start, self[2])) def subperiods(self, unit: str) -> Sequence[Period]: """Return the list of all the periods of unit ``unit``. @@ -534,26 +650,42 @@ def subperiods(self, unit: str) -> Sequence[Period]: ValueError: If the period's unit is smaller than the given unit. Examples: - >>> period = Period((YEAR, Instant((2021, 1, 1)), 1)) + >>> from openfisca_core import periods + + >>> start = periods.Instant((2021, 1, 1)) + + >>> period = Period((YEAR, start, 1)) >>> period.subperiods(MONTH) [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] - >>> period = Period((YEAR, Instant((2021, 1, 1)), 2)) + >>> period = Period((YEAR, start, 2)) >>> period.subperiods(YEAR) [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] + .. versionchanged:: 39.0.0: + Renamed from ``get_subperiods`` to ``subperiods``. + """ if UNIT_WEIGHTS[self.unit] < UNIT_WEIGHTS[unit]: raise ValueError(f"Cannot subdivide {self.unit} into {unit}") if unit == YEAR: - return [self.this_year.offset(i, YEAR) for i in range(self.size)] + return [ + self.this(YEAR).offset(offset, YEAR) + for offset in range(self.size) + ] if unit == MONTH: - return [self.first_month.offset(i, MONTH) for i in range(self.size_in_months)] + return [ + self.this(MONTH).offset(offset, MONTH) + for offset in range(self.size_in_months) + ] if unit == DAY: - return [self.first_day.offset(i, DAY) for i in range(self.size_in_days)] + return [ + self.this(DAY).offset(offset, DAY) + for offset in range(self.size_in_days) + ] raise DateUnitValueError(unit) diff --git a/openfisca_core/periods/tests/test_funcs.py b/openfisca_core/periods/tests/test_funcs.py index 5ae894f973..e21d1daa05 100644 --- a/openfisca_core/periods/tests/test_funcs.py +++ b/openfisca_core/periods/tests/test_funcs.py @@ -6,53 +6,54 @@ @pytest.mark.parametrize("arg, expected", [ - ["eternity", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], - ["ETERNITY", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], - [periods.ETERNITY, periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], - [periods.Instant((1, 1, 1)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 1))], - [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365))], - [1000, periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], ["1004-02-29", periods.Period((periods.DAY, periods.Instant((1004, 2, 29)), 1))], + ["ETERNITY", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], + ["day:1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["day:1000-01-01:3", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 3))], + ["eternity", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], + ["month:1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["month:1000-01-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["month:1000-01-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], + ["month:1000-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], ["year:1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["year:1000:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], ["year:1000-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["year:1000-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], ["year:1000-01-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], ["year:1000-01-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], - ["month:1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["month:1000-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], - ["month:1000-01-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["month:1000-01-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], - ["day:1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], - ["day:1000-01-01:3", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 3))], + ["year:1000-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], + ["year:1000:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], + [1000, periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + [periods.ETERNITY, periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], + [periods.Instant((1, 1, 1)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 1))], + [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.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", [ - [None, ValueError], - [periods.YEAR, ValueError], - [datetime.date(1, 1, 1), ValueError], - ["1000:1", ValueError], ["1000-0", ValueError], - ["1000-1", ValueError], - ["1000-13", ValueError], - ["1000-01:1", 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-01-01:1", ValueError], - ["month:1000", ValueError], - ["month:1000:1", ValueError], - ["day:1000:1", ValueError], + ["1000:1", ValueError], ["day:1000-01", ValueError], ["day:1000-01:1", ValueError], + ["day:1000:1", ValueError], + ["month:1000", ValueError], + ["month:1000:1", ValueError], + [None, ValueError], + [datetime.date(1, 1, 1), ValueError], + [periods.YEAR, ValueError], ]) def test_build_period_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" @@ -63,26 +64,28 @@ def test_build_period_with_an_invalid_argument(arg, error): @pytest.mark.parametrize("arg, expected", [ ["1", None], - ["999", None], ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], - ["1000-1", None], - ["1000-1-1", None], ["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 @pytest.mark.parametrize("arg, expected", [ [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), "100_365"], + [periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1)), "400_1"], [periods.Period((periods.MONTH, periods.Instant((1, 1, 1)), 12)), "200_12"], [periods.Period((periods.YEAR, periods.Instant((1, 1, 1)), 2)), "300_2"], - [periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1)), "400_1"], ]) def test_key_period_size_with_a_valid_argument(arg, expected): """Returns the corresponding period's weight.""" + assert periods.key_period_size(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 44d953732d..4ae497b835 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -8,23 +8,25 @@ @pytest.fixture def instant(): """Returns a ``Instant``.""" + return periods.Instant((2020, 2, 29)) @pytest.mark.parametrize("offset, unit, expected", [ - ["first-of", periods.YEAR, periods.Instant((2020, 1, 1))], ["first-of", periods.MONTH, periods.Instant((2020, 2, 1))], - ["last-of", periods.YEAR, periods.Instant((2020, 12, 31))], + ["first-of", periods.YEAR, periods.Instant((2020, 1, 1))], ["last-of", periods.MONTH, periods.Instant((2020, 2, 29))], - [-3, periods.YEAR, periods.Instant((2017, 2, 28))], - [-3, periods.MONTH, periods.Instant((2019, 11, 29))], + ["last-of", periods.YEAR, periods.Instant((2020, 12, 31))], [-3, periods.DAY, periods.Instant((2020, 2, 26))], - [3, periods.YEAR, periods.Instant((2023, 2, 28))], - [3, periods.MONTH, periods.Instant((2020, 5, 29))], + [-3, periods.MONTH, periods.Instant((2019, 11, 29))], + [-3, periods.YEAR, periods.Instant((2017, 2, 28))], [3, periods.DAY, periods.Instant((2020, 3, 3))], + [3, periods.MONTH, periods.Instant((2020, 5, 29))], + [3, periods.YEAR, periods.Instant((2023, 2, 28))], ]) def test_offset(instant, offset, unit, expected): """Returns the expected ``Instant``.""" + assert instant.offset(offset, unit) == expected @@ -40,36 +42,37 @@ def test_offset_with_an_invalid_offset(instant, offset, unit, expected): @pytest.mark.parametrize("arg, expected", [ - [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.DAY, periods.Instant((1, 1, 1)), 365)), periods.Instant((1, 1, 1))], - [1000, periods.Instant((1000, 1, 1))], ["1000", periods.Instant((1000, 1, 1))], ["1000-01", periods.Instant((1000, 1, 1))], ["1000-01-01", periods.Instant((1000, 1, 1))], + [1000, periods.Instant((1000, 1, 1))], + [datetime.date(1, 1, 1), periods.Instant((1, 1, 1))], + [periods.Instant((1, 1, 1)), periods.Instant((1, 1, 1))], + [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Instant((1, 1, 1))], ]) def test_build_instant(arg, expected): """Returns the expected ``Instant``.""" + assert periods.Instant.build(arg) == expected @pytest.mark.parametrize("arg, error", [ - [periods.YEAR, ValueError], - [periods.ETERNITY, ValueError], ["1000-0", ValueError], - ["1000-1", ValueError], - ["1000-13", ValueError], ["1000-0-0", ValueError], - ["1000-1-1", ValueError], ["1000-01-0", ValueError], ["1000-01-1", ValueError], ["1000-01-32", ValueError], + ["1000-1", ValueError], + ["1000-1-1", ValueError], + ["1000-13", ValueError], ["month:1000", ValueError], ["month:1000:1", ValueError], ["year:1000-01-01", ValueError], ["year:1000-01-01:1", ValueError], ["year:1000-01-01:3", ValueError], + [None, TypeError], + [periods.ETERNITY, ValueError], + [periods.YEAR, ValueError], ]) def test_build_instant_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 60c068c567..fa5430a1ac 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -6,19 +6,21 @@ @pytest.fixture def instant(): """Returns a ``Instant``.""" + return periods.Instant((2022, 12, 31)) @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.YEAR, periods.Instant((2022, 1, 1)), 1, "2022"], [periods.MONTH, periods.Instant((2022, 1, 1)), 12, "2022"], - [periods.YEAR, periods.Instant((2022, 3, 1)), 1, "year:2022-03"], [periods.MONTH, periods.Instant((2022, 3, 1)), 12, "year:2022-03"], + [periods.YEAR, periods.Instant((2022, 1, 1)), 1, "2022"], [periods.YEAR, periods.Instant((2022, 1, 1)), 3, "year:2022:3"], [periods.YEAR, periods.Instant((2022, 1, 3)), 3, "year:2022:3"], + [periods.YEAR, periods.Instant((2022, 3, 1)), 1, "year:2022-03"], ]) def test_str_with_years(date_unit, instant, size, expected): """Returns the expected string.""" + assert str(periods.Period((date_unit, instant, size))) == expected @@ -29,6 +31,7 @@ def test_str_with_years(date_unit, instant, size, expected): ]) def test_str_with_months(date_unit, instant, size, expected): """Returns the expected string.""" + assert str(periods.Period((date_unit, instant, size))) == expected @@ -39,16 +42,17 @@ def test_str_with_months(date_unit, instant, size, expected): ]) 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("period_unit, unit, start, cease, count", [ - [periods.YEAR, periods.YEAR, periods.Instant((2022, 1, 1)), periods.Instant((2024, 1, 1)), 3], - [periods.YEAR, periods.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2025, 11, 1)), 36], - [periods.YEAR, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2025, 12, 30)), 1096], - [periods.MONTH, periods.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2023, 2, 1)), 3], - [periods.MONTH, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2023, 3, 30)), 90], [periods.DAY, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2023, 1, 2)), 3], + [periods.MONTH, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2023, 3, 30)), 90], + [periods.MONTH, periods.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2023, 2, 1)), 3], + [periods.YEAR, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2025, 12, 30)), 1096], + [periods.YEAR, periods.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2025, 11, 1)), 36], + [periods.YEAR, periods.YEAR, periods.Instant((2022, 1, 1)), periods.Instant((2024, 1, 1)), 3], ]) def test_subperiods(instant, period_unit, unit, start, cease, count): """Returns the expected subperiods.""" @@ -62,27 +66,27 @@ def test_subperiods(instant, period_unit, unit, start, cease, count): @pytest.mark.parametrize("period_unit, offset, unit, expected", [ - [periods.YEAR, "first-of", periods.YEAR, periods.Period(('year', periods.Instant((2022, 1, 1)), 3))], - [periods.YEAR, "first-of", periods.MONTH, periods.Period(('year', periods.Instant((2022, 12, 1)), 3))], - [periods.YEAR, "last-of", periods.YEAR, periods.Period(('year', periods.Instant((2022, 12, 31)), 3))], - [periods.YEAR, "last-of", periods.MONTH, periods.Period(('year', periods.Instant((2022, 12, 31)), 3))], - [periods.YEAR, -3, periods.YEAR, periods.Period(('year', periods.Instant((2019, 12, 31)), 3))], - [periods.YEAR, 1, periods.MONTH, periods.Period(('year', periods.Instant((2023, 1, 31)), 3))], - [periods.YEAR, 3, periods.DAY, periods.Period(('year', periods.Instant((2023, 1, 3)), 3))], - [periods.MONTH, "first-of", periods.YEAR, periods.Period(('month', periods.Instant((2022, 1, 1)), 3))], - [periods.MONTH, "first-of", periods.MONTH, periods.Period(('month', periods.Instant((2022, 12, 1)), 3))], - [periods.MONTH, "last-of", periods.YEAR, periods.Period(('month', periods.Instant((2022, 12, 31)), 3))], - [periods.MONTH, "last-of", periods.MONTH, periods.Period(('month', periods.Instant((2022, 12, 31)), 3))], - [periods.MONTH, -3, periods.YEAR, periods.Period(('month', periods.Instant((2019, 12, 31)), 3))], - [periods.MONTH, 1, periods.MONTH, periods.Period(('month', periods.Instant((2023, 1, 31)), 3))], - [periods.MONTH, 3, periods.DAY, periods.Period(('month', periods.Instant((2023, 1, 3)), 3))], - [periods.DAY, "first-of", periods.YEAR, periods.Period(('day', periods.Instant((2022, 1, 1)), 3))], - [periods.DAY, "first-of", periods.MONTH, periods.Period(('day', periods.Instant((2022, 12, 1)), 3))], - [periods.DAY, "last-of", periods.YEAR, periods.Period(('day', periods.Instant((2022, 12, 31)), 3))], - [periods.DAY, "last-of", periods.MONTH, periods.Period(('day', periods.Instant((2022, 12, 31)), 3))], - [periods.DAY, -3, periods.YEAR, periods.Period(('day', periods.Instant((2019, 12, 31)), 3))], - [periods.DAY, 1, periods.MONTH, periods.Period(('day', periods.Instant((2023, 1, 31)), 3))], - [periods.DAY, 3, periods.DAY, periods.Period(('day', periods.Instant((2023, 1, 3)), 3))], + [periods.DAY, "first-of", periods.MONTH, periods.Period(("day", periods.Instant((2022, 12, 1)), 3))], + [periods.DAY, "first-of", periods.YEAR, periods.Period(("day", periods.Instant((2022, 1, 1)), 3))], + [periods.DAY, "last-of", periods.MONTH, periods.Period(("day", periods.Instant((2022, 12, 31)), 3))], + [periods.DAY, "last-of", periods.YEAR, periods.Period(("day", periods.Instant((2022, 12, 31)), 3))], + [periods.DAY, -3, periods.YEAR, periods.Period(("day", periods.Instant((2019, 12, 31)), 3))], + [periods.DAY, 1, periods.MONTH, periods.Period(("day", periods.Instant((2023, 1, 31)), 3))], + [periods.DAY, 3, periods.DAY, periods.Period(("day", periods.Instant((2023, 1, 3)), 3))], + [periods.MONTH, "first-of", periods.MONTH, periods.Period(("month", periods.Instant((2022, 12, 1)), 3))], + [periods.MONTH, "first-of", periods.YEAR, periods.Period(("month", periods.Instant((2022, 1, 1)), 3))], + [periods.MONTH, "last-of", periods.MONTH, periods.Period(("month", periods.Instant((2022, 12, 31)), 3))], + [periods.MONTH, "last-of", periods.YEAR, periods.Period(("month", periods.Instant((2022, 12, 31)), 3))], + [periods.MONTH, -3, periods.YEAR, periods.Period(("month", periods.Instant((2019, 12, 31)), 3))], + [periods.MONTH, 1, periods.MONTH, periods.Period(("month", periods.Instant((2023, 1, 31)), 3))], + [periods.MONTH, 3, periods.DAY, periods.Period(("month", periods.Instant((2023, 1, 3)), 3))], + [periods.YEAR, "first-of", periods.MONTH, periods.Period(("year", periods.Instant((2022, 12, 1)), 3))], + [periods.YEAR, "first-of", periods.YEAR, periods.Period(("year", periods.Instant((2022, 1, 1)), 3))], + [periods.YEAR, "last-of", periods.MONTH, periods.Period(("year", periods.Instant((2022, 12, 31)), 3))], + [periods.YEAR, "last-of", periods.YEAR, periods.Period(("year", periods.Instant((2022, 12, 31)), 3))], + [periods.YEAR, -3, periods.YEAR, periods.Period(("year", periods.Instant((2019, 12, 31)), 3))], + [periods.YEAR, 1, periods.MONTH, periods.Period(("year", periods.Instant((2023, 1, 31)), 3))], + [periods.YEAR, 3, periods.DAY, periods.Period(("year", periods.Instant((2023, 1, 3)), 3))], ]) def test_offset(instant, period_unit, offset, unit, expected): """Returns the expected ``Period``.""" @@ -93,13 +97,13 @@ def test_offset(instant, period_unit, offset, unit, expected): @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, periods.Instant((2022, 12, 1)), 1, 1], + [periods.MONTH, periods.Instant((2012, 1, 3)), 3, 3], [periods.MONTH, periods.Instant((2012, 2, 3)), 1, 1], [periods.MONTH, periods.Instant((2022, 1, 3)), 3, 3], - [periods.MONTH, periods.Instant((2012, 1, 3)), 3, 3], - [periods.YEAR, periods.Instant((2022, 12, 1)), 1, 12], + [periods.MONTH, periods.Instant((2022, 12, 1)), 1, 1], [periods.YEAR, periods.Instant((2012, 1, 1)), 1, 12], [periods.YEAR, periods.Instant((2022, 1, 1)), 2, 24], + [periods.YEAR, periods.Instant((2022, 12, 1)), 1, 12], ]) def test_day_size_in_months(date_unit, instant, size, expected): """Returns the expected number of months.""" @@ -112,13 +116,13 @@ def test_day_size_in_months(date_unit, instant, size, expected): @pytest.mark.parametrize("date_unit, instant, size, expected", [ [periods.DAY, periods.Instant((2022, 12, 31)), 1, 1], [periods.DAY, periods.Instant((2022, 12, 31)), 3, 3], - [periods.MONTH, periods.Instant((2022, 12, 1)), 1, 31], + [periods.MONTH, periods.Instant((2012, 1, 3)), 3, 31 + 29 + 31], [periods.MONTH, periods.Instant((2012, 2, 3)), 1, 29], [periods.MONTH, periods.Instant((2022, 1, 3)), 3, 31 + 28 + 31], - [periods.MONTH, periods.Instant((2012, 1, 3)), 3, 31 + 29 + 31], - [periods.YEAR, periods.Instant((2022, 12, 1)), 1, 365], + [periods.MONTH, periods.Instant((2022, 12, 1)), 1, 31], [periods.YEAR, periods.Instant((2012, 1, 1)), 1, 366], [periods.YEAR, periods.Instant((2022, 1, 1)), 2, 730], + [periods.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.""" diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index 93e0afe381..fdf2bccb83 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -2,8 +2,10 @@ from __future__ import annotations +import datetime + import typing_extensions -from typing import Any +from typing import Any, Iterable, Iterator from typing_extensions import Protocol import abc @@ -11,12 +13,22 @@ @typing_extensions.runtime_checkable class Instant(Protocol): - @property + def __init__(cls, *args: Iterable[int]) -> None: ... + + @abc.abstractmethod + def __iter__(self) -> Iterator[int]: ... + + @abc.abstractmethod + def __ge__(self, other: object) -> bool: ... + + @abc.abstractmethod + def __le__(self, other: object) -> bool: ... + @abc.abstractmethod - def date(self) -> Any: ... + def date(self) -> datetime.date: ... @abc.abstractmethod - def offset(self, offset: Any, unit: Any) -> Any: ... + def offset(self, offset: str | int, unit: str) -> Instant: ... @typing_extensions.runtime_checkable diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index a92686f984..a551ec76c1 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -172,7 +172,7 @@ def calculate_add(self, variable_name: str, period): # Check that the requested period matches definition_period 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( + 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 @@ -209,7 +209,7 @@ def calculate_divide(self, variable_name: str, period): raise ValueError("DIVIDE option can only be used for a one-year or a one-month requested period") if period.unit == periods.MONTH: - computation_period = period.this_year + computation_period = period.this(YEAR) return self.calculate(variable_name, period = computation_period) / 12. elif period.unit == periods.YEAR: return self.calculate(variable_name, period) @@ -276,7 +276,7 @@ def _check_period_consistency(self, period, variable): )) if variable.definition_period == periods.YEAR and period.unit != periods.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( + 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 )) diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index d3aa4e1e05..253e97a7c9 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -1,25 +1,29 @@ from __future__ import annotations +import typing +from openfisca_core.periods.typing import Instant, Period 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 + +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.variables import Variable diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index 7ae026bb99..d86f5f8131 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -18,7 +18,7 @@ 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 period not in annualization_period): - return population(variable.name, period.this_year.first_month) + return population(variable.name, period.this(YEAR).first_month) if original_formula.__code__.co_argcount == 2: return original_formula(population, period) return original_formula(population, period, parameters) diff --git a/setup.py b/setup.py index 7be9792e5f..6d7c03ea39 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ 'PyYAML >= 3.10', 'dpath >= 1.5.0, < 3.0.0', 'importlib-metadata < 4.3.0', + 'inflect >= 6.0.0, < 7.0.0', 'nptyping == 1.4.4', 'numexpr >= 2.7.0, <= 3.0', 'numpy >= 1.20, < 1.21', diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index 70390e783c..89f7db6550 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -51,7 +51,7 @@ def test_non_existing_variable(simulation): @pytest.mark.parametrize("simulation", [({}, PERIOD)], indirect = True) def test_calculate_variable_with_wrong_definition_period(simulation): - year = str(PERIOD.this_year) + year = str(PERIOD.this(YEAR)) with pytest.raises(ValueError) as error: simulation.calculate("basic_income", year) @@ -84,7 +84,7 @@ def test_divide_option_with_complex_period(simulation): def test_input_with_wrong_period(tax_benefit_system): - year = str(PERIOD.this_year) + year = str(PERIOD.this(YEAR)) variables = {"basic_income": {year: 12000}} simulation_builder = SimulationBuilder() simulation_builder.set_default_period(PERIOD) From ae23f091735770145bd6eb1afef13cf9f0bcbc3f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 06:20:21 +0100 Subject: [PATCH 51/93] Refactor size_in_x --- openfisca_core/periods/period_.py | 202 +++++------------------------- 1 file changed, 33 insertions(+), 169 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 2eb5c3d8f1..09daaa4e67 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -232,122 +232,6 @@ def size(self) -> int: return self[2] - @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: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 10, 1)) - - >>> period = Period((YEAR, start, 3)) - >>> period.size_in_days - 1096 - - >>> period = Period((MONTH, start, 3)) - >>> period.size_in_days - 92 - - """ - - if self.unit == DAY: - return self.size - - if self.unit in (MONTH, YEAR): - return (self.stop.date() - self.start.date()).days + 1 - - raise ValueError(f"Cannot calculate number of days in {self.unit}.") - - @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: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 10, 1)) - - >>> period = Period((YEAR, start, 3)) - >>> period.size_in_months - 36 - - >>> period = Period((DAY, start, 3)) - >>> period.size_in_months - Traceback (most recent call last): - ValueError: Cannot calculate number of months in day. - - """ - - if self.unit == MONTH: - return self.size - - if self.unit == YEAR: - return self.size * 12 - - raise ValueError(f"Cannot calculate number of months in {self[0]}.") - - @property - def size_in_years(self) -> int: - """The ``size`` of the ``Period`` in years. - - Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 10, 1)) - - >>> period = Period((YEAR, start, 3)) - >>> period.size_in_years - 3 - - >>> period = Period((MONTH, start, 3)) - >>> period.size_in_years - Traceback (most recent call last): - ValueError: Cannot calculate number of years in month. - - """ - - if self.unit == YEAR: - return self.size - - raise ValueError(f"Cannot calculate number of years in {self.unit}.") - - @property - def days(self) -> int: - """Count the number of days in period. - - Returns: - An int. - - Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 10, 1)) - - >>> period = Period((YEAR, start, 3)) - >>> period.days - 1096 - - >>> period = Period((MONTH, start, 3)) - >>> period.days - 92 - - """ - - return self.size_in_days - @property def stop(self) -> Instant: """Last day of the ``Period`` as an ``Instant``. @@ -388,29 +272,6 @@ def stop(self) -> Instant: return type(start)((stop.year, stop.month, stop.day)) - @property - def today(self) -> Period: - """A new day ``Period`` representing today. - - Returns: - A Period. - - Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2023, 1, 1)) - - >>> period = Period((YEAR, start, 3)) - - >>> period.today - Period(('day', Instant((2023, 1, 1)), 1)) - - .. versionadded:: 39.0.0 - - """ - - return self.this(DAY) - def date(self) -> datetime.Date: """The date representation of the ``period``'s' start date. @@ -444,7 +305,7 @@ def date(self) -> datetime.Date: return self.start.date() - def size_in(self, unit: str) -> int: + def count(self, unit: str) -> int: """The ``size`` of the ``Period`` in the given unit. Args: @@ -459,31 +320,46 @@ def size_in(self, unit: str) -> int: Examples: >>> from openfisca_core import periods - >>> start = periods.Instant((2022, 1, 1)) + >>> start = periods.Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) - - >>> period.size_in(DAY) + >>> period.count(DAY) 1096 - >>> period.size_in(MONTH) + >>> period = Period((MONTH, start, 3)) + >>> period.count(DAY) + 92 + + >>> period = Period((YEAR, start, 3)) + >>> period.count(MONTH) 36 - >>> period.size_in(YEAR) + >>> period = Period((DAY, start, 3)) + >>> period.count(MONTH) + Traceback (most recent call last): + ValueError: Cannot calculate number of months in day. + + >>> period = Period((YEAR, start, 3)) + >>> period.count(YEAR) 3 + >>> period = Period((MONTH, start, 3)) + >>> period.count(YEAR) + Traceback (most recent call last): + ValueError: Cannot calculate number of years in month. + .. versionadded:: 39.0.0 """ - if unit == DAY: - return self.size_in_days + if unit == self.unit: + return self.size - if unit == MONTH: - return self.size_in_months + if unit == DAY and self.unit in (MONTH, YEAR): + return (self.stop.date() - self.start.date()).days + 1 - if unit == YEAR: - return self.size_in_years + if unit == MONTH and self.unit == YEAR: + return self.size * 12 raise ValueError(f"Cannot calculate number of {unit} in {self.unit}.") @@ -670,22 +546,10 @@ def subperiods(self, unit: str) -> Sequence[Period]: if UNIT_WEIGHTS[self.unit] < UNIT_WEIGHTS[unit]: raise ValueError(f"Cannot subdivide {self.unit} into {unit}") - if unit == YEAR: - return [ - self.this(YEAR).offset(offset, YEAR) - for offset in range(self.size) - ] - - if unit == MONTH: - return [ - self.this(MONTH).offset(offset, MONTH) - for offset in range(self.size_in_months) - ] - - if unit == DAY: - return [ - self.this(DAY).offset(offset, DAY) - for offset in range(self.size_in_days) - ] + if unit not in (DAY, MONTH, YEAR): + raise DateUnitValueError(unit) - raise DateUnitValueError(unit) + return [ + self.this(unit).offset(offset, unit) + for offset in range(self.count(unit)) + ] From 2f4503efb49f0d10032a84b371a586c0b6be7da9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 06:31:24 +0100 Subject: [PATCH 52/93] Fix tests --- openfisca_core/periods/instant_.py | 35 ++++++++------------ openfisca_core/periods/period_.py | 4 +-- openfisca_core/periods/tests/test_instant.py | 11 ------ openfisca_core/periods/tests/test_period.py | 4 +-- 4 files changed, 17 insertions(+), 37 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 86331ee990..870489b873 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,13 +1,12 @@ from __future__ import annotations -from typing import Any, Tuple +from typing import Any, Callable, Tuple import calendar import datetime import functools -from dateutil import relativedelta - +import inflect import pendulum from ._config import INSTANT_PATTERN @@ -76,6 +75,8 @@ class Instant(Tuple[int, int, int]): """ + plural: Callable[[str], str] = inflect.engine().plural + def __repr__(self) -> str: return ( f"{type(self).__name__}" @@ -141,10 +142,10 @@ def date(self) -> datetime.date: Example: >>> instant = Instant((2021, 10, 1)) >>> instant.date() - datetime.date(2021, 10, 1) + Date(2021, 10, 1) Returns: - A datetime.time. + A date. """ @@ -165,16 +166,16 @@ def offset(self, offset: str | int, unit: str) -> Instant: OffsetTypeError: When ``offset`` is of type ``int``. Examples: - >>> Instant((2020, 12, 31)).offset("first-of", "month") + >>> Instant((2020, 12, 31)).offset("first-of", MONTH) Instant((2020, 12, 1)) - >>> Instant((2020, 1, 1)).offset("last-of", "year") + >>> Instant((2020, 1, 1)).offset("last-of", YEAR) Instant((2020, 12, 31)) - >>> Instant((2020, 1, 1)).offset(1, "year") + >>> Instant((2020, 1, 1)).offset(1, YEAR) Instant((2021, 1, 1)) - >>> Instant((2020, 1, 1)).offset(-3, "day") + >>> Instant((2020, 1, 1)).offset(-3, DAY) Instant((2019, 12, 29)) """ @@ -203,19 +204,9 @@ def offset(self, offset: str | int, unit: str) -> Instant: if not isinstance(offset, int): raise OffsetTypeError(offset) - if unit == DAY: - date = self.date() + relativedelta.relativedelta(days = offset) - return type(self)((date.year, date.month, date.day)) - - if unit == MONTH: - date = self.date() + relativedelta.relativedelta(months = offset) - return type(self)((date.year, date.month, date.day)) - - if unit == YEAR: - date = self.date() + relativedelta.relativedelta(years = offset) - return type(self)((date.year, date.month, date.day)) + date = self.date().add(**{self.plural(unit): offset}) - return self + return type(self)((date.year, date.month, date.day)) @classmethod def build(cls, value: Any) -> Instant: @@ -251,7 +242,7 @@ def build(cls, value: Any) -> Instant: Instant((2021, 9, 1)) >>> start = periods.Instant((2021, 9, 16)) - >>> period = periods.Period(("year", start, 1)) + >>> period = periods.Period((YEAR, start, 1)) >>> periods.Instant.build(period) Instant((2021, 9, 16)) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 09daaa4e67..4226cfa5e5 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -3,7 +3,7 @@ from typing import Callable, Sequence, Tuple import inflect -from pendulum import datetime +import datetime from ._errors import DateUnitValueError from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR @@ -272,7 +272,7 @@ def stop(self) -> Instant: return type(start)((stop.year, stop.month, stop.day)) - def date(self) -> datetime.Date: + def date(self) -> datetime.date: """The date representation of the ``period``'s' start date. Returns: diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 4ae497b835..594d98fb6f 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -30,17 +30,6 @@ def test_offset(instant, offset, unit, expected): assert instant.offset(offset, unit) == expected -@pytest.mark.parametrize("offset, unit, expected", [ - ["first-of", periods.DAY, TypeError], - ["last-of", periods.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) - - @pytest.mark.parametrize("arg, expected", [ ["1000", periods.Instant((1000, 1, 1))], ["1000-01", periods.Instant((1000, 1, 1))], diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index fa5430a1ac..e400d33007 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -110,7 +110,7 @@ def test_day_size_in_months(date_unit, instant, size, expected): period = periods.Period((date_unit, instant, size)) - assert period.size_in_months == expected + assert period.count("month") == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ @@ -129,4 +129,4 @@ def test_day_size_in_days(date_unit, instant, size, expected): period = periods.Period((date_unit, instant, size)) - assert period.size_in_days == expected + assert period.count("day") == expected From 77c86ba4412ecb0253f3b3f346f1e46a9d3d15e8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 06:47:22 +0100 Subject: [PATCH 53/93] Adapt codebase to changes in Instant --- CHANGELOG.md | 3 ++- openfisca_core/periods/period_.py | 16 +++++++++++----- openfisca_core/simulations/simulation.py | 2 +- openfisca_core/variables/helpers.py | 8 +++++--- tests/core/test_countries.py | 6 +++--- tests/core/test_cycles.py | 10 +++++----- 6 files changed, 27 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72cada064b..24dd4fdcb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,10 @@ - Refactor `Period.contains` as `Period.__contains__`. - Rename `Period.get_subperiods` to `subperiods`. - Rename `instant` to `Instant.build`. -- Rename `period` to `build_period`. +- Rename `period` to `Instant.period`. - Transform `Instant.date` from property to method. - Transform `Period.date` from property to method. +- Simplify reference periods. #### Technical changes diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 4226cfa5e5..251647e7b2 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -337,7 +337,7 @@ def count(self, unit: str) -> int: >>> period = Period((DAY, start, 3)) >>> period.count(MONTH) Traceback (most recent call last): - ValueError: Cannot calculate number of months in day. + ValueError: Cannot calculate number of months in a day. >>> period = Period((YEAR, start, 3)) >>> period.count(YEAR) @@ -346,7 +346,7 @@ def count(self, unit: str) -> int: >>> period = Period((MONTH, start, 3)) >>> period.count(YEAR) Traceback (most recent call last): - ValueError: Cannot calculate number of years in month. + ValueError: Cannot calculate number of years in a month. .. versionadded:: 39.0.0 @@ -361,7 +361,10 @@ def count(self, unit: str) -> int: if unit == MONTH and self.unit == YEAR: return self.size * 12 - raise ValueError(f"Cannot calculate number of {unit} in {self.unit}.") + raise ValueError( + f"Cannot calculate number of {self.plural(unit)} in a " + f"{self.unit}." + ) def this(self, unit: str) -> Period: """A new month ``Period`` starting at the first of ``unit``. @@ -508,9 +511,12 @@ def offset(self, offset: str | int, unit: str | None = None) -> Period: """ - start = self[1].offset(offset, self[0] if unit is None else unit) + if unit is None: + unit = self.unit + + start = self.start.offset(offset, unit) - return type(self)((self[0], start, self[2])) + return type(self)((self.unit, start, self.size)) def subperiods(self, unit: str) -> Sequence[Period]: """Return the list of all the periods of unit ``unit``. diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index a551ec76c1..c441c9bf8f 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -209,7 +209,7 @@ def calculate_divide(self, variable_name: str, period): raise ValueError("DIVIDE option can only be used for a one-year or a one-month requested period") if period.unit == periods.MONTH: - computation_period = period.this(YEAR) + computation_period = period.this(periods.YEAR) return self.calculate(variable_name, period = computation_period) / 12. elif period.unit == periods.YEAR: return self.calculate(variable_name, period) diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index d86f5f8131..5131c538b3 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -1,9 +1,11 @@ from __future__ import annotations -import sortedcontainers +from openfisca_core.periods.typing import Period from typing import Optional -from openfisca_core.periods import Period +import sortedcontainers + +from openfisca_core import periods from .. import variables @@ -18,7 +20,7 @@ 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 period not in annualization_period): - return population(variable.name, period.this(YEAR).first_month) + return population(variable.name, period.this(periods.YEAR).this(periods.MONTH)) if original_formula.__code__.co_argcount == 2: return original_formula(population, period) return original_formula(population, period, parameters) diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index 89f7db6550..f5d21db260 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -51,7 +51,7 @@ def test_non_existing_variable(simulation): @pytest.mark.parametrize("simulation", [({}, PERIOD)], indirect = True) def test_calculate_variable_with_wrong_definition_period(simulation): - year = str(PERIOD.this(YEAR)) + year = str(PERIOD.this(periods.YEAR)) with pytest.raises(ValueError) as error: simulation.calculate("basic_income", year) @@ -71,7 +71,7 @@ def test_divide_option_on_month_defined_variable(simulation): @pytest.mark.parametrize("simulation", [({}, PERIOD)], indirect = True) def test_divide_option_with_complex_period(simulation): - quarter = PERIOD.last_3_months + quarter = PERIOD.last(periods.MONTH, 3) with pytest.raises(ValueError) as error: simulation.household("housing_tax", quarter, options = [populations.DIVIDE]) @@ -84,7 +84,7 @@ def test_divide_option_with_complex_period(simulation): def test_input_with_wrong_period(tax_benefit_system): - year = str(PERIOD.this(YEAR)) + year = str(PERIOD.this(periods.YEAR)) variables = {"basic_income": {year: 12000}} simulation_builder = SimulationBuilder() simulation_builder.set_default_period(PERIOD) diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index dc17aee5de..35044de5c5 100644 --- a/tests/core/test_cycles.py +++ b/tests/core/test_cycles.py @@ -44,7 +44,7 @@ class variable3(Variable): definition_period = periods.MONTH def formula(person, period): - return person('variable4', period.last_month) + return person('variable4', period.last(periods.MONTH)) class variable4(Variable): @@ -64,7 +64,7 @@ class variable5(Variable): definition_period = periods.MONTH def formula(person, period): - variable6 = person('variable6', period.last_month) + variable6 = person('variable6', period.last(periods.MONTH)) return 5 + variable6 @@ -96,7 +96,7 @@ class cotisation(Variable): def formula(person, period): if period.start.month == 12: - return 2 * person('cotisation', period.last_month) + return 2 * person('cotisation', period.last(periods.MONTH)) else: return person.empty_array() + 1 @@ -128,7 +128,7 @@ def test_spirals_result_in_default_value(simulation, reference_period): def test_spiral_heuristic(simulation, reference_period): variable5 = simulation.calculate('variable5', period = reference_period) variable6 = simulation.calculate('variable6', period = reference_period) - variable6_last_month = simulation.calculate('variable6', reference_period.last_month) + variable6_last_month = simulation.calculate('variable6', reference_period.last(periods.MONTH)) tools.assert_near(variable5, [11]) tools.assert_near(variable6, [11]) tools.assert_near(variable6_last_month, [11]) @@ -141,6 +141,6 @@ def test_spiral_cache(simulation, reference_period): def test_cotisation_1_level(simulation, reference_period): - month = reference_period.last_month + month = reference_period.last(periods.MONTH) cotisation = simulation.calculate('cotisation', period = month) tools.assert_near(cotisation, [0]) From 1ee083752404550235430612d276b0bf1c237a0c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 07:29:36 +0100 Subject: [PATCH 54/93] Refactor unit weights --- openfisca_core/periods/__init__.py | 2 +- openfisca_core/periods/_units.py | 10 +++++++++- openfisca_core/periods/helpers.py | 14 +++++++------- openfisca_core/periods/instant_.py | 2 +- openfisca_core/periods/period_.py | 7 ++++--- openfisca_core/periods/tests/test_funcs.py | 8 ++++---- openfisca_core/periods/typing.py | 3 +-- openfisca_core/simulations/simulation.py | 2 +- 8 files changed, 28 insertions(+), 20 deletions(-) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index fa73ca8f8d..1659036561 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -28,7 +28,7 @@ """ from ._config import INSTANT_PATTERN -from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR +from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR from .helpers import build_period, key_period_size, parse_period from .instant_ import Instant from .period_ import Period diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_units.py index d3f6a04cfd..c62f56fdb7 100644 --- a/openfisca_core/periods/_units.py +++ b/openfisca_core/periods/_units.py @@ -1,5 +1,13 @@ +import enum + DAY = "day" MONTH = "month" YEAR = "year" ETERNITY = "eternity" -UNIT_WEIGHTS = {DAY: 100, MONTH: 200, YEAR: 300, ETERNITY: 400} + + +class DateUnit(enum.IntFlag): + day = enum.auto() + month = enum.auto() + year = enum.auto() + eternity = enum.auto() diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 06256c66a4..a98295f45a 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -9,7 +9,7 @@ from pendulum.parsing import ParserError from ._errors import PeriodFormatError -from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR +from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR from .instant_ import Instant from .period_ import Period @@ -117,7 +117,7 @@ def build_period(value: Any) -> Period: raise PeriodFormatError(value) # Reject ambiguous periods such as month:2014 - if UNIT_WEIGHTS[base_period.unit] > UNIT_WEIGHTS[unit]: + if DateUnit[base_period.unit] > DateUnit[unit]: raise PeriodFormatError(value) return Period((unit, base_period.start, size)) @@ -137,19 +137,19 @@ def key_period_size(period: Period) -> str: Examples: >>> instant = Instant((2021, 9, 14)) - >>> period = Period(("day", instant, 1)) + >>> period = Period((DAY, instant, 1)) >>> key_period_size(period) - '100_1' + '1_1' - >>> period = Period(("year", instant, 3)) + >>> period = Period((YEAR, instant, 3)) >>> key_period_size(period) - '300_3' + '4_3' """ unit, _start, size = period - return f"{UNIT_WEIGHTS[unit]}_{size}" + return f"{DateUnit[unit]}_{size}" def parse_period(value: str) -> Period | None: diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 870489b873..109e52831d 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -13,8 +13,8 @@ from ._errors import ( DateUnitValueError, InstantFormatError, - InstantValueError, InstantTypeError, + InstantValueError, OffsetTypeError, ) from ._units import DAY, MONTH, YEAR diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 251647e7b2..2074e06629 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -2,11 +2,12 @@ from typing import Callable, Sequence, Tuple -import inflect import datetime +import inflect + from ._errors import DateUnitValueError -from ._units import DAY, ETERNITY, MONTH, UNIT_WEIGHTS, YEAR +from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR from .typing import Instant @@ -549,7 +550,7 @@ def subperiods(self, unit: str) -> Sequence[Period]: """ - if UNIT_WEIGHTS[self.unit] < UNIT_WEIGHTS[unit]: + if DateUnit[self.unit] < DateUnit[unit]: raise ValueError(f"Cannot subdivide {self.unit} into {unit}") if unit not in (DAY, MONTH, YEAR): diff --git a/openfisca_core/periods/tests/test_funcs.py b/openfisca_core/periods/tests/test_funcs.py index e21d1daa05..35fd6f292a 100644 --- a/openfisca_core/periods/tests/test_funcs.py +++ b/openfisca_core/periods/tests/test_funcs.py @@ -80,10 +80,10 @@ def test_parse_period(arg, expected): @pytest.mark.parametrize("arg, expected", [ - [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), "100_365"], - [periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1)), "400_1"], - [periods.Period((periods.MONTH, periods.Instant((1, 1, 1)), 12)), "200_12"], - [periods.Period((periods.YEAR, periods.Instant((1, 1, 1)), 2)), "300_2"], + [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), "1_365"], + [periods.Period((periods.MONTH, periods.Instant((1, 1, 1)), 12)), "2_12"], + [periods.Period((periods.YEAR, periods.Instant((1, 1, 1)), 2)), "4_2"], + [periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1)), "8_1"], ]) def test_key_period_size_with_a_valid_argument(arg, expected): """Returns the corresponding period's weight.""" diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index fdf2bccb83..67eb615277 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -2,13 +2,12 @@ from __future__ import annotations -import datetime - import typing_extensions from typing import Any, Iterable, Iterator from typing_extensions import Protocol import abc +import datetime @typing_extensions.runtime_checkable diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index c441c9bf8f..26eeb51a2a 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -171,7 +171,7 @@ def calculate_add(self, variable_name: str, period): period = periods.build_period(period) # Check that the requested period matches definition_period - if periods.UNIT_WEIGHTS[variable.definition_period] > periods.UNIT_WEIGHTS[period.unit]: + if periods.DateUnit[variable.definition_period] > periods.DateUnit[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, From 8275a59c5a7f7c004ca644fd1d42e81a7b4a3d57 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 07:41:18 +0100 Subject: [PATCH 55/93] Deprecate key_value_period --- CHANGELOG.md | 3 +- openfisca_core/periods/__init__.py | 2 +- openfisca_core/periods/helpers.py | 29 ------------------- openfisca_core/periods/period_.py | 1 - openfisca_core/periods/tests/test_funcs.py | 12 -------- .../simulations/simulation_builder.py | 3 +- 6 files changed, 5 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24dd4fdcb4..00ff9349a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,12 @@ - Deprecate `periods.intersect`. - Deprecate `periods.unit_weight`. - Deprecate `periods.unit_weights`. +- Deprecate `periods.key_period_size`. - 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 `Instant.build`. -- Rename `period` to `Instant.period`. +- Rename `period` to `Period.build`. - Transform `Instant.date` from property to method. - Transform `Period.date` from property to method. - Simplify reference periods. diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 1659036561..69cb497a17 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -29,6 +29,6 @@ from ._config import INSTANT_PATTERN from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR -from .helpers import build_period, key_period_size, parse_period +from .helpers import build_period, parse_period from .instant_ import Instant from .period_ import Period diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index a98295f45a..93f0164547 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -123,35 +123,6 @@ def build_period(value: Any) -> Period: return Period((unit, base_period.start, size)) -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: An :mod:`.openfisca_core` :obj:`.Period`. - - Returns: - :obj:`str`: A string. - - Examples: - >>> instant = Instant((2021, 9, 14)) - - >>> period = Period((DAY, instant, 1)) - >>> key_period_size(period) - '1_1' - - >>> period = Period((YEAR, instant, 3)) - >>> key_period_size(period) - '4_3' - - """ - - unit, _start, size = period - - return f"{DateUnit[unit]}_{size}" - - def parse_period(value: str) -> Period | None: """Parse periods respecting the ISO format. diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 2074e06629..9f0f6197a4 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -398,7 +398,6 @@ def this(self, unit: str) -> Period: return type(self)((unit, self.start.offset("first-of", unit), 1)) - def last(self, unit: str, size: int = 1) -> Period: """Last ``size`` ``unit``s of the ``Period``. diff --git a/openfisca_core/periods/tests/test_funcs.py b/openfisca_core/periods/tests/test_funcs.py index 35fd6f292a..82ae11804e 100644 --- a/openfisca_core/periods/tests/test_funcs.py +++ b/openfisca_core/periods/tests/test_funcs.py @@ -77,15 +77,3 @@ def test_parse_period(arg, expected): """Returns an ``Instant`` when given a valid ISO format string.""" assert periods.parse_period(arg) == expected - - -@pytest.mark.parametrize("arg, expected", [ - [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), "1_365"], - [periods.Period((periods.MONTH, periods.Instant((1, 1, 1)), 12)), "2_12"], - [periods.Period((periods.YEAR, periods.Instant((1, 1, 1)), 2)), "4_2"], - [periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1)), "8_1"], - ]) -def test_key_period_size_with_a_valid_argument(arg, expected): - """Returns the corresponding period's weight.""" - - assert periods.key_period_size(arg) == expected diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index 615a6995a4..e745573190 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -413,7 +413,8 @@ def finalize_variables_init(self, population): buffer = self.input_buffer[variable_name] 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) + sorted_periods = sorted(unsorted_periods, key = lambda period: f"{periods.DateUnit[period.unit].value}_{period.size}") + for period_value in sorted_periods: values = buffer[str(period_value)] # Hack to replicate the values in the persons entity From d465f54c3db7936fa76598918adaddd784ca1114 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 09:41:00 +0100 Subject: [PATCH 56/93] Refactor date units --- openfisca_core/periods/_errors.py | 24 ++-- openfisca_core/periods/_units.py | 110 +++++++++++++++++-- openfisca_core/periods/helpers.py | 58 +++++----- openfisca_core/periods/instant_.py | 12 +- openfisca_core/periods/period_.py | 70 ++++++------ openfisca_core/periods/tests/test_instant.py | 4 +- openfisca_core/periods/tests/test_period.py | 46 ++++---- 7 files changed, 213 insertions(+), 111 deletions(-) diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index 43fc9e3e1a..dd96b1475f 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -15,8 +15,9 @@ class DateUnitValueError(ValueError): 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 '{YEAR}'. {LEARN_MORE}" + f"'{str(value)}' is not a valid ISO format date unit. ISO format " + f"date units are any of: '{str(DAY)}', '{str(MONTH)}', or " + f"'{str(YEAR)}'. {LEARN_MORE}" ) @@ -25,8 +26,9 @@ class InstantFormatError(ValueError): 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}" + f"'{str(value)}' is not a valid instant. Instants are described " + "using the 'YYYY-MM-DD' format, for instance '2015-06-15'. " + f"{LEARN_MORE}" ) @@ -35,7 +37,7 @@ class InstantValueError(ValueError): def __init__(self, value: Any) -> None: super().__init__( - f"Invalid instant: '{value}' has a length of {len(value)}. " + f"Invalid instant: '{str(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}" @@ -47,8 +49,8 @@ class InstantTypeError(TypeError): 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}" + f"Invalid instant: '{str(value)}' of type {type(value)}, " + f"expecting an 'Instant', 'tuple', or 'list'. {LEARN_MORE}" ) @@ -57,8 +59,8 @@ class PeriodFormatError(ValueError): 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"'{str(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}" ) @@ -68,6 +70,6 @@ class OffsetTypeError(TypeError): def __init__(self, value: Any) -> None: super().__init__( - f"Invalid offset: {value} of type {type(value)}, expecting an " - f"'int'. {LEARN_MORE}" + f"Invalid offset: '{str(value)}' of type {type(value)}, expecting " + f"an 'int'. {LEARN_MORE}" ) diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_units.py index c62f56fdb7..8dee6e4545 100644 --- a/openfisca_core/periods/_units.py +++ b/openfisca_core/periods/_units.py @@ -1,13 +1,105 @@ +from __future__ import annotations + import enum -DAY = "day" -MONTH = "month" -YEAR = "year" -ETERNITY = "eternity" + +class DateUnitMeta(enum.EnumMeta): + @property + def isoformat(self) -> DateUnit: + """Date units corresponding to the ISO format (day, month, and year). + + Returns: + A DateUnit representing ISO format units. + + Examples: + >>> DateUnit.isoformat + + + >>> DateUnit.DAY in DateUnit.isoformat + True + + >>> DateUnit.ETERNITY in DateUnit.isoformat + False + + .. versionadded:: 39.0.0 + + """ + + return ~DateUnit.ETERNITY + + +class DateUnit(enum.IntFlag, metaclass = DateUnitMeta): + """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: 1} + + >>> list(DateUnit) + [day, month, year, eternity] + + >>> len(DateUnit) + 4 + + >>> DateUnit["DAY"] + day + + >>> DateUnit(DateUnit.DAY) + day + + >>> DateUnit.DAY in DateUnit + True + + >>> "DAY" in DateUnit + False + + >>> DateUnit.DAY == 1 + True + + >>> DateUnit.DAY.name + 'DAY' + + >>> DateUnit.DAY.value + 1 + + .. versionadded:: 39.0.0 + + """ + + #: The day unit. + DAY = enum.auto() + + #: The month unit. + MONTH = enum.auto() + + #: The year unit. + YEAR = enum.auto() + + #: A special unit to represent time-independent properties. + ETERNITY = enum.auto() + + def __repr__(self) -> str: + try: + return self.name.lower() + + except AttributeError: + return super().__repr__() + + def __str__(self) -> str: + try: + return self.name.lower() + + except AttributeError: + return super().__str__() -class DateUnit(enum.IntFlag): - day = enum.auto() - month = enum.auto() - year = enum.auto() - eternity = enum.auto() +DAY, MONTH, YEAR, ETERNITY = DateUnit diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 93f0164547..d8dfc6fce4 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -29,35 +29,35 @@ def build_period(value: Any) -> Period: PeriodFormatError: When the arguments were invalid, like "2021-32-13". Examples: - >>> build_period(Period(("year", Instant((2021, 1, 1)), 1))) - Period(('year', Instant((2021, 1, 1)), 1)) + >>> build_period(Period((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)) + Period((day, Instant((2021, 1, 1)), 1)) - >>> build_period("eternity") - Period(('eternity', Instant((1, 1, 1)), 1)) + >>> build_period(ETERNITY) + Period((eternity, Instant((1, 1, 1)), 1)) >>> build_period(2021) - Period(('year', Instant((2021, 1, 1)), 1)) + Period((year, Instant((2021, 1, 1)), 1)) >>> build_period("2014") - Period(('year', Instant((2014, 1, 1)), 1)) + Period((year, Instant((2014, 1, 1)), 1)) >>> build_period("year:2014") - Period(('year', Instant((2014, 1, 1)), 1)) + Period((year, Instant((2014, 1, 1)), 1)) >>> build_period("month:2014-02") - Period(('month', Instant((2014, 2, 1)), 1)) + Period((month, Instant((2014, 2, 1)), 1)) >>> build_period("year:2014-02") - Period(('year', Instant((2014, 2, 1)), 1)) + Period((year, Instant((2014, 2, 1)), 1)) >>> build_period("day:2014-02-02") - Period(('day', Instant((2014, 2, 2)), 1)) + Period((day, Instant((2014, 2, 2)), 1)) >>> build_period("day:2014-02-02:3") - Period(('day', Instant((2014, 2, 2)), 3)) + Period((day, Instant((2014, 2, 2)), 3)) """ @@ -67,8 +67,8 @@ def build_period(value: Any) -> Period: if isinstance(value, Instant): return Period((DAY, value, 1)) - if value == "ETERNITY" or value == ETERNITY: - return Period(("eternity", Instant.build(datetime.date.min), 1)) + if value in {ETERNITY, ETERNITY.name}: + return Period((ETERNITY, Instant.build(datetime.date.min), 1)) if isinstance(value, int): return Period((YEAR, Instant((value, 1, 1)), 1)) @@ -89,15 +89,15 @@ def build_period(value: Any) -> Period: components = value.split(":") # Left-most component must be a valid unit - unit = components[0] + unit = DateUnit[components[0].upper()] - if unit not in (DAY, MONTH, YEAR): + if unit not in DateUnit.isoformat: raise PeriodFormatError(value) - # Middle component must be a valid iso period - base_period = parse_period(components[1]) + # Middle component must be a valid ISO period + period = parse_period(components[1]) - if not base_period: + if not period: raise PeriodFormatError(value) # Periods like year:2015-03 have a size of 1 @@ -117,10 +117,10 @@ def build_period(value: Any) -> Period: raise PeriodFormatError(value) # Reject ambiguous periods such as month:2014 - if DateUnit[base_period.unit] > DateUnit[unit]: + if period.unit > unit: raise PeriodFormatError(value) - return Period((unit, base_period.start, size)) + return Period((unit, period.start, size)) def parse_period(value: str) -> Period | None: @@ -138,13 +138,13 @@ def parse_period(value: str) -> Period | None: Examples: >>> parse_period("2022") - Period(('year', Instant((2022, 1, 1)), 1)) + Period((year, Instant((2022, 1, 1)), 1)) >>> parse_period("2022-02") - Period(('month', Instant((2022, 2, 1)), 1)) + Period((month, Instant((2022, 2, 1)), 1)) >>> parse_period("2022-02-13") - Period(('day', Instant((2022, 2, 13)), 1)) + Period((day, Instant((2022, 2, 13)), 1)) """ @@ -169,6 +169,14 @@ def parse_period(value: str) -> Period | None: if not isinstance(date, Date): raise ValueError - unit = UNIT_MAPPING[len(value.split("-"))] + # We get the shape of the string (e.g. "2012-02" = 2) + size = len(value.split("-")) + + # We get the unit from the shape (e.g. 2 = "month") + unit = DateUnit(pow(2, 3 - size)) + + # We build the corresponding start instant start = Instant((date.year, date.month, date.day)) + + # And return the period return Period((unit, start, 1)) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 109e52831d..fa8c6b1a6e 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -17,7 +17,7 @@ InstantValueError, OffsetTypeError, ) -from ._units import DAY, MONTH, YEAR +from ._units import DateUnit, DAY, MONTH, YEAR from .typing import Period @@ -56,10 +56,10 @@ class Instant(Tuple[int, int, int]): All the rest of the ``tuple`` protocols are inherited as well: - >>> instant[0] + >>> instant.year 2021 - >>> instant[0] in instant + >>> instant.year in instant True >>> len(instant) @@ -182,10 +182,10 @@ def offset(self, offset: str | int, unit: str) -> Instant: year, month, day = self - if unit not in (DAY, MONTH, YEAR): + if unit not in DateUnit.isoformat: raise DateUnitValueError(unit) - if offset in ("first-of", "last-of") and unit == DAY: + if offset in {"first-of", "last-of"} and unit == DAY: return self if offset == "first-of" and unit == MONTH: @@ -249,7 +249,7 @@ def build(cls, value: Any) -> Instant: .. versionadded:: 39.0.0 """ - if value is None: + if value is None or isinstance(value, DateUnit): raise InstantTypeError(value) if isinstance(value, Instant): diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 9f0f6197a4..01648798c7 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -11,7 +11,7 @@ from .typing import Instant -class Period(Tuple[str, Instant, int]): +class Period(Tuple[DateUnit, Instant, int]): """Toolbox to handle date intervals. A ``Period`` is a triple (``unit``, ``start``, ``size``). @@ -38,7 +38,7 @@ class Period(Tuple[str, Instant, int]): an ``Instant`` and the ``size``: >>> repr(period) - "Period(('year', Instant((2021, 9, 1)), 3))" + '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``: @@ -55,10 +55,10 @@ class Period(Tuple[str, Instant, int]): All the rest of the ``tuple`` protocols are inherited as well: - >>> period[0] - 'year' + >>> period.unit + year - >>> period[0] in period + >>> period.unit in period True >>> len(period) @@ -119,7 +119,7 @@ def __str__(self) -> str: unit, start, size = self if unit == ETERNITY: - return "ETERNITY" + return unit.name year, month, day = start @@ -131,7 +131,7 @@ def __str__(self) -> str: else: # rolling year - return f"{YEAR}:{year}-{month:02d}" + return f"{str(YEAR)}:{year}-{month:02d}" # simple month if unit == MONTH and size == 1: @@ -139,17 +139,17 @@ def __str__(self) -> str: # several civil years if unit == YEAR and month == 1: - return f"{unit}:{year}:{size}" + return f"{str(unit)}:{year}:{size}" if unit == DAY: if size == 1: return f"{year}-{month:02d}-{day:02d}" else: - return f"{unit}:{year}-{month:02d}-{day:02d}:{size}" + return f"{str(unit)}:{year}-{month:02d}-{day:02d}:{size}" # complex period - return f"{unit}:{year}-{month:02d}:{size}" + return f"{str(unit)}:{year}-{month:02d}:{size}" def __contains__(self, other: object) -> bool: """Checks if a ``period`` contains another one. @@ -189,7 +189,7 @@ def unit(self) -> str: >>> start = periods.Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) >>> period.unit - 'year' + year """ @@ -356,7 +356,7 @@ def count(self, unit: str) -> int: if unit == self.unit: return self.size - if unit == DAY and self.unit in (MONTH, YEAR): + if unit == DAY and self.unit in {MONTH, YEAR}: return (self.stop.date() - self.start.date()).days + 1 if unit == MONTH and self.unit == YEAR: @@ -384,13 +384,13 @@ def this(self, unit: str) -> Period: >>> period = Period((YEAR, start, 3)) >>> period.this(DAY) - Period(('day', Instant((2023, 1, 1)), 1)) + Period((day, Instant((2023, 1, 1)), 1)) >>> period.this(MONTH) - Period(('month', Instant((2023, 1, 1)), 1)) + Period((month, Instant((2023, 1, 1)), 1)) >>> period.this(YEAR) - Period(('year', Instant((2023, 1, 1)), 1)) + Period((year, Instant((2023, 1, 1)), 1)) .. versionadded:: 39.0.0 @@ -416,22 +416,22 @@ def last(self, unit: str, size: int = 1) -> Period: >>> period = Period((YEAR, start, 3)) >>> period.last(DAY) - Period(('day', Instant((2022, 12, 31)), 1)) + Period((day, Instant((2022, 12, 31)), 1)) >>> period.last(DAY, 7) - Period(('day', Instant((2022, 12, 25)), 7)) + Period((day, Instant((2022, 12, 25)), 7)) >>> period.last(MONTH) - Period(('month', Instant((2022, 12, 1)), 1)) + Period((month, Instant((2022, 12, 1)), 1)) >>> period.last(MONTH, 3) - Period(('month', Instant((2022, 10, 1)), 3)) + Period((month, Instant((2022, 10, 1)), 3)) >>> period.last(YEAR) - Period(('year', Instant((2022, 1, 1)), 1)) + Period((year, Instant((2022, 1, 1)), 1)) >>> period.last(YEAR, 1) - Period(('year', Instant((2022, 1, 1)), 1)) + Period((year, Instant((2022, 1, 1)), 1)) .. versionadded:: 39.0.0 @@ -457,22 +457,22 @@ def ago(self, unit: str, size: int = 1) -> Period: >>> period = Period((YEAR, start, 3)) >>> period.ago(DAY) - Period(('day', Instant((2022, 12, 31)), 1)) + Period((day, Instant((2022, 12, 31)), 1)) >>> period.ago(DAY, 7) - Period(('day', Instant((2022, 12, 25)), 1)) + Period((day, Instant((2022, 12, 25)), 1)) >>> period.ago(MONTH) - Period(('month', Instant((2022, 12, 1)), 1)) + Period((month, Instant((2022, 12, 1)), 1)) >>> period.ago(MONTH, 3) - Period(('month', Instant((2022, 10, 1)), 1)) + Period((month, Instant((2022, 10, 1)), 1)) >>> period.ago(YEAR) - Period(('year', Instant((2022, 1, 1)), 1)) + Period((year, Instant((2022, 1, 1)), 1)) >>> period.ago(YEAR, 1) - Period(('year', Instant((2022, 1, 1)), 1)) + Period((year, Instant((2022, 1, 1)), 1)) .. versionadded:: 39.0.0 @@ -496,18 +496,18 @@ def offset(self, offset: str | int, unit: str | None = None) -> Period: >>> start = periods.Instant((2014, 2, 3)) >>> Period((DAY, start, 1)).offset("first-of", MONTH) - Period(('day', Instant((2014, 2, 1)), 1)) + Period((day, Instant((2014, 2, 1)), 1)) >>> Period((MONTH, start, 4)).offset("last-of", MONTH) - Period(('month', Instant((2014, 2, 28)), 4)) + Period((month, Instant((2014, 2, 28)), 4)) >>> start = periods.Instant((2021, 1, 1)) >>> Period((DAY, start, 365)).offset(-3) - Period(('day', Instant((2020, 12, 29)), 365)) + Period((day, Instant((2020, 12, 29)), 365)) >>> Period((DAY, start, 365)).offset(1, YEAR) - Period(('day', Instant((2022, 1, 1)), 365)) + Period((day, Instant((2022, 1, 1)), 365)) """ @@ -538,21 +538,21 @@ def subperiods(self, unit: str) -> Sequence[Period]: >>> period = Period((YEAR, start, 1)) >>> period.subperiods(MONTH) - [Period(('month', Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] + [Period((month, Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] >>> period = Period((YEAR, start, 2)) >>> period.subperiods(YEAR) - [Period(('year', Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] + [Period((year, Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] .. versionchanged:: 39.0.0: Renamed from ``get_subperiods`` to ``subperiods``. """ - if DateUnit[self.unit] < DateUnit[unit]: + if self.unit < unit: raise ValueError(f"Cannot subdivide {self.unit} into {unit}") - if unit not in (DAY, MONTH, YEAR): + if unit not in DateUnit.isoformat: raise DateUnitValueError(unit) return [ diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 594d98fb6f..3ec05d5965 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -60,8 +60,8 @@ def test_build_instant(arg, expected): ["year:1000-01-01:1", ValueError], ["year:1000-01-01:3", ValueError], [None, TypeError], - [periods.ETERNITY, ValueError], - [periods.YEAR, ValueError], + [periods.ETERNITY, TypeError], + [periods.YEAR, TypeError], ]) def test_build_instant_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index e400d33007..7edcfbf793 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -66,27 +66,27 @@ def test_subperiods(instant, period_unit, unit, start, cease, count): @pytest.mark.parametrize("period_unit, offset, unit, expected", [ - [periods.DAY, "first-of", periods.MONTH, periods.Period(("day", periods.Instant((2022, 12, 1)), 3))], - [periods.DAY, "first-of", periods.YEAR, periods.Period(("day", periods.Instant((2022, 1, 1)), 3))], - [periods.DAY, "last-of", periods.MONTH, periods.Period(("day", periods.Instant((2022, 12, 31)), 3))], - [periods.DAY, "last-of", periods.YEAR, periods.Period(("day", periods.Instant((2022, 12, 31)), 3))], - [periods.DAY, -3, periods.YEAR, periods.Period(("day", periods.Instant((2019, 12, 31)), 3))], - [periods.DAY, 1, periods.MONTH, periods.Period(("day", periods.Instant((2023, 1, 31)), 3))], - [periods.DAY, 3, periods.DAY, periods.Period(("day", periods.Instant((2023, 1, 3)), 3))], - [periods.MONTH, "first-of", periods.MONTH, periods.Period(("month", periods.Instant((2022, 12, 1)), 3))], - [periods.MONTH, "first-of", periods.YEAR, periods.Period(("month", periods.Instant((2022, 1, 1)), 3))], - [periods.MONTH, "last-of", periods.MONTH, periods.Period(("month", periods.Instant((2022, 12, 31)), 3))], - [periods.MONTH, "last-of", periods.YEAR, periods.Period(("month", periods.Instant((2022, 12, 31)), 3))], - [periods.MONTH, -3, periods.YEAR, periods.Period(("month", periods.Instant((2019, 12, 31)), 3))], - [periods.MONTH, 1, periods.MONTH, periods.Period(("month", periods.Instant((2023, 1, 31)), 3))], - [periods.MONTH, 3, periods.DAY, periods.Period(("month", periods.Instant((2023, 1, 3)), 3))], - [periods.YEAR, "first-of", periods.MONTH, periods.Period(("year", periods.Instant((2022, 12, 1)), 3))], - [periods.YEAR, "first-of", periods.YEAR, periods.Period(("year", periods.Instant((2022, 1, 1)), 3))], - [periods.YEAR, "last-of", periods.MONTH, periods.Period(("year", periods.Instant((2022, 12, 31)), 3))], - [periods.YEAR, "last-of", periods.YEAR, periods.Period(("year", periods.Instant((2022, 12, 31)), 3))], - [periods.YEAR, -3, periods.YEAR, periods.Period(("year", periods.Instant((2019, 12, 31)), 3))], - [periods.YEAR, 1, periods.MONTH, periods.Period(("year", periods.Instant((2023, 1, 31)), 3))], - [periods.YEAR, 3, periods.DAY, periods.Period(("year", periods.Instant((2023, 1, 3)), 3))], + [periods.DAY, "first-of", periods.MONTH, periods.Period((periods.DAY, periods.Instant((2022, 12, 1)), 3))], + [periods.DAY, "first-of", periods.YEAR, periods.Period((periods.DAY, periods.Instant((2022, 1, 1)), 3))], + [periods.DAY, "last-of", periods.MONTH, periods.Period((periods.DAY, periods.Instant((2022, 12, 31)), 3))], + [periods.DAY, "last-of", periods.YEAR, periods.Period((periods.DAY, periods.Instant((2022, 12, 31)), 3))], + [periods.DAY, -3, periods.YEAR, periods.Period((periods.DAY, periods.Instant((2019, 12, 31)), 3))], + [periods.DAY, 1, periods.MONTH, periods.Period((periods.DAY, periods.Instant((2023, 1, 31)), 3))], + [periods.DAY, 3, periods.DAY, periods.Period((periods.DAY, periods.Instant((2023, 1, 3)), 3))], + [periods.MONTH, "first-of", periods.MONTH, periods.Period((periods.MONTH, periods.Instant((2022, 12, 1)), 3))], + [periods.MONTH, "first-of", periods.YEAR, periods.Period((periods.MONTH, periods.Instant((2022, 1, 1)), 3))], + [periods.MONTH, "last-of", periods.MONTH, periods.Period((periods.MONTH, periods.Instant((2022, 12, 31)), 3))], + [periods.MONTH, "last-of", periods.YEAR, periods.Period((periods.MONTH, periods.Instant((2022, 12, 31)), 3))], + [periods.MONTH, -3, periods.YEAR, periods.Period((periods.MONTH, periods.Instant((2019, 12, 31)), 3))], + [periods.MONTH, 1, periods.MONTH, periods.Period((periods.MONTH, periods.Instant((2023, 1, 31)), 3))], + [periods.MONTH, 3, periods.DAY, periods.Period((periods.MONTH, periods.Instant((2023, 1, 3)), 3))], + [periods.YEAR, "first-of", periods.MONTH, periods.Period((periods.YEAR, periods.Instant((2022, 12, 1)), 3))], + [periods.YEAR, "first-of", periods.YEAR, periods.Period((periods.YEAR, periods.Instant((2022, 1, 1)), 3))], + [periods.YEAR, "last-of", periods.MONTH, periods.Period((periods.YEAR, periods.Instant((2022, 12, 31)), 3))], + [periods.YEAR, "last-of", periods.YEAR, periods.Period((periods.YEAR, periods.Instant((2022, 12, 31)), 3))], + [periods.YEAR, -3, periods.YEAR, periods.Period((periods.YEAR, periods.Instant((2019, 12, 31)), 3))], + [periods.YEAR, 1, periods.MONTH, periods.Period((periods.YEAR, periods.Instant((2023, 1, 31)), 3))], + [periods.YEAR, 3, periods.DAY, periods.Period((periods.YEAR, periods.Instant((2023, 1, 3)), 3))], ]) def test_offset(instant, period_unit, offset, unit, expected): """Returns the expected ``Period``.""" @@ -110,7 +110,7 @@ def test_day_size_in_months(date_unit, instant, size, expected): period = periods.Period((date_unit, instant, size)) - assert period.count("month") == expected + assert period.count(periods.MONTH) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ @@ -129,4 +129,4 @@ def test_day_size_in_days(date_unit, instant, size, expected): period = periods.Period((date_unit, instant, size)) - assert period.count("day") == expected + assert period.count(periods.DAY) == expected From fd648d404973c2612d98957c029f8129f3c94ce3 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 10:20:35 +0100 Subject: [PATCH 57/93] Fix remaining tests --- openfisca_core/periods/_errors.py | 12 +++++- openfisca_core/periods/_kinds.py | 9 ++++ openfisca_core/periods/helpers.py | 48 ++++++++++++++-------- openfisca_core/periods/tests/test_funcs.py | 4 +- 4 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 openfisca_core/periods/_kinds.py diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index dd96b1475f..c629ee4772 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -50,7 +50,7 @@ class InstantTypeError(TypeError): def __init__(self, value: Any) -> None: super().__init__( f"Invalid instant: '{str(value)}' of type {type(value)}, " - f"expecting an 'Instant', 'tuple', or 'list'. {LEARN_MORE}" + f"expecting an 'Instant', 'tuple', 'list', or 'str'. {LEARN_MORE}" ) @@ -65,6 +65,16 @@ def __init__(self, value: Any) -> None: ) +class PeriodTypeError(TypeError): + """Raised when a period's type is not valid.""" + + def __init__(self, value: Any) -> None: + super().__init__( + f"Invalid period: '{str(value)}' of type {type(value)}, " + f"expecting a 'Period', 'tuple', 'list', or 'str. {LEARN_MORE}" + ) + + class OffsetTypeError(TypeError): """Raised when an offset's type is not valid.""" diff --git a/openfisca_core/periods/_kinds.py b/openfisca_core/periods/_kinds.py new file mode 100644 index 0000000000..200756fead --- /dev/null +++ b/openfisca_core/periods/_kinds.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from typing import NamedTuple + + +class ISOFormat(NamedTuple): + year: int + month: int + day: int diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index d8dfc6fce4..f3ed04365a 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -8,7 +8,7 @@ from pendulum.datetime import Date from pendulum.parsing import ParserError -from ._errors import PeriodFormatError +from ._errors import PeriodFormatError, PeriodTypeError from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR from .instant_ import Instant from .period_ import Period @@ -61,15 +61,18 @@ def build_period(value: Any) -> Period: """ + if value in {ETERNITY, ETERNITY.name, ETERNITY.name.lower()}: + return Period((ETERNITY, Instant.build(datetime.date.min), 1)) + + if value is None or isinstance(value, DateUnit): + raise PeriodTypeError(value) + if isinstance(value, Period): return value if isinstance(value, Instant): return Period((DAY, value, 1)) - if value in {ETERNITY, ETERNITY.name}: - return Period((ETERNITY, Instant.build(datetime.date.min), 1)) - if isinstance(value, int): return Period((YEAR, Instant((value, 1, 1)), 1)) @@ -86,34 +89,43 @@ def build_period(value: Any) -> Period: if ":" not in value: raise PeriodFormatError(value) - components = value.split(":") + # We know the first element has to be a ``unit`` + unit, *rest = value.split(":") - # Left-most component must be a valid unit - unit = DateUnit[components[0].upper()] + # Units are case insensitive so we need to upper them + unit = unit.upper() - if unit not in DateUnit.isoformat: + # Left-most component must be a valid unit + if unit not in dir(DateUnit): raise PeriodFormatError(value) + unit = DateUnit[unit] + + # We get the first remaining component + period, *rest = rest + # Middle component must be a valid ISO period - period = parse_period(components[1]) + period = parse_period(period) - if not period: + if not isinstance(period, Period): raise PeriodFormatError(value) - # Periods like year:2015-03 have a size of 1 - if len(components) == 2: + # Finally we try to parse the size, if any + try: + size, *rest = rest + + except ValueError: size = 1 # If provided, make sure the size is an integer - elif len(components) == 3: - try: - size = int(components[2]) + try: + size = int(size) - except ValueError: - raise PeriodFormatError(value) + except ValueError: + raise PeriodFormatError(value) # If there are more than 2 ":" in the string, the period is invalid - else: + if len(rest) > 0: raise PeriodFormatError(value) # Reject ambiguous periods such as month:2014 diff --git a/openfisca_core/periods/tests/test_funcs.py b/openfisca_core/periods/tests/test_funcs.py index 82ae11804e..a01ee090f0 100644 --- a/openfisca_core/periods/tests/test_funcs.py +++ b/openfisca_core/periods/tests/test_funcs.py @@ -51,9 +51,9 @@ def test_build_period(arg, expected): ["day:1000:1", ValueError], ["month:1000", ValueError], ["month:1000:1", ValueError], - [None, ValueError], + [None, TypeError], [datetime.date(1, 1, 1), ValueError], - [periods.YEAR, ValueError], + [periods.YEAR, TypeError], ]) def test_build_period_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" From 06d0c24918609478e3d154c3d19fb5ba7c0cc64b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 11:43:33 +0100 Subject: [PATCH 58/93] Replace the parsing function with the parser --- openfisca_core/periods/__init__.py | 3 +- openfisca_core/periods/_kinds.py | 9 --- openfisca_core/periods/_parsers.py | 76 ++++++++++++++++++ openfisca_core/periods/helpers.py | 78 ++----------------- openfisca_core/periods/instant_.py | 9 ++- .../tests/{test_funcs.py => test__parsers.py} | 12 +-- openfisca_core/periods/tests/test_instant.py | 4 + 7 files changed, 100 insertions(+), 91 deletions(-) delete mode 100644 openfisca_core/periods/_kinds.py create mode 100644 openfisca_core/periods/_parsers.py rename openfisca_core/periods/tests/{test_funcs.py => test__parsers.py} (88%) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 69cb497a17..f168b9609a 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -28,7 +28,8 @@ """ from ._config import INSTANT_PATTERN +from ._parsers import ISOFormat from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR -from .helpers import build_period, parse_period +from .helpers import build_period from .instant_ import Instant from .period_ import Period diff --git a/openfisca_core/periods/_kinds.py b/openfisca_core/periods/_kinds.py deleted file mode 100644 index 200756fead..0000000000 --- a/openfisca_core/periods/_kinds.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -from typing import NamedTuple - - -class ISOFormat(NamedTuple): - year: int - month: int - day: int diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py new file mode 100644 index 0000000000..1f362ee6fd --- /dev/null +++ b/openfisca_core/periods/_parsers.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from typing import NamedTuple + +import pendulum +from pendulum.datetime import Date +from pendulum.parsing import ParserError + + +class ISOFormat(NamedTuple): + unit: int + year: int + month: int + day: int + shape: int + + @classmethod + def parse(cls, value: str) -> ISOFormat | None: + """Parse strings respecting the ISO format. + + Args: + value: A string such as such as "2012" or "2015-03". + + Returns: + An ISOFormat object if ``value`` is valid. + None if ``value`` is not valid. + + Raises: + AttributeError: When arguments are invalid, like ``"-1"``. + ValueError: When values are invalid, like ``"2022-32-13"``. + + Examples: + >>> ISOFormat.parse("ETERNITY") + + >>> ISOFormat.parse("2022") + ISOFormat(unit=4, year=2022, month=1, day=1, shape=1) + + >>> ISOFormat.parse("2022-02") + ISOFormat(unit=2, year=2022, month=2, day=1, shape=2) + + >>> ISOFormat.parse("2022-02-13") + ISOFormat(unit=1, year=2022, month=2, day=13, shape=3) + + .. versionadded:: 39.0.0 + + """ + + # If it's a complex period, next! + if len(value.split(":")) != 1: + return None + + # Check for a non-empty string. + if not value and not 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 + + # We get the shape of the string (e.g. "2012-02" = 2) + shape = len(value.split("-")) + + # We get the unit from the shape (e.g. 2 = "month") + unit = pow(2, 3 - shape) + + # We build the corresponding ISOFormat object + return cls(unit, date.year, date.month, date.day, shape) diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index f3ed04365a..e58ed87eaf 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -4,17 +4,12 @@ import datetime -import pendulum -from pendulum.datetime import Date -from pendulum.parsing import ParserError - from ._errors import PeriodFormatError, PeriodTypeError +from ._parsers import ISOFormat from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR from .instant_ import Instant from .period_ import Period -UNIT_MAPPING = {1: "year", 2: "month", 3: "day"} - def build_period(value: Any) -> Period: """Build a new period, aka a triple (unit, start_instant, size). @@ -80,10 +75,10 @@ def build_period(value: Any) -> Period: raise PeriodFormatError(value) # Try to parse as a simple period - period = parse_period(value) + period = ISOFormat.parse(value) if period is not None: - return period + return Period((DateUnit(period.unit), Instant((period[1:-1])), 1)) # Complex periods must have a ':' in their strings if ":" not in value: @@ -105,9 +100,9 @@ def build_period(value: Any) -> Period: period, *rest = rest # Middle component must be a valid ISO period - period = parse_period(period) + period = ISOFormat.parse(period) - if not isinstance(period, Period): + if period is None: raise PeriodFormatError(value) # Finally we try to parse the size, if any @@ -124,7 +119,7 @@ def build_period(value: Any) -> Period: except ValueError: raise PeriodFormatError(value) - # If there are more than 2 ":" in the string, the period is invalid + # If there were more than 2 ":" in the string, the period is invalid if len(rest) > 0: raise PeriodFormatError(value) @@ -132,63 +127,4 @@ def build_period(value: Any) -> Period: if period.unit > unit: raise PeriodFormatError(value) - return Period((unit, period.start, size)) - - -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 - - # We get the shape of the string (e.g. "2012-02" = 2) - size = len(value.split("-")) - - # We get the unit from the shape (e.g. 2 = "month") - unit = DateUnit(pow(2, 3 - size)) - - # We build the corresponding start instant - start = Instant((date.year, date.month, date.day)) - - # And return the period - return Period((unit, start, 1)) + return Period((unit, Instant((period[1:-1])), size)) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index fa8c6b1a6e..71ae0356be 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -17,6 +17,7 @@ InstantValueError, OffsetTypeError, ) +from ._parsers import ISOFormat from ._units import DateUnit, DAY, MONTH, YEAR from .typing import Period @@ -261,11 +262,11 @@ def build(cls, value: Any) -> Instant: if isinstance(value, str) and not INSTANT_PATTERN.match(value): raise InstantFormatError(value) + if isinstance(value, str) and len(value.split("-")) > 3: + raise InstantValueError(value) + if isinstance(value, str): - instant = tuple( - int(fragment) - for fragment in value.split('-', 2)[:3] - ) + instant = ISOFormat.parse(value)[1:-1] elif isinstance(value, datetime.date): instant = value.year, value.month, value.day diff --git a/openfisca_core/periods/tests/test_funcs.py b/openfisca_core/periods/tests/test__parsers.py similarity index 88% rename from openfisca_core/periods/tests/test_funcs.py rename to openfisca_core/periods/tests/test__parsers.py index a01ee090f0..4faeed4d27 100644 --- a/openfisca_core/periods/tests/test_funcs.py +++ b/openfisca_core/periods/tests/test__parsers.py @@ -64,16 +64,16 @@ def test_build_period_with_an_invalid_argument(arg, error): @pytest.mark.parametrize("arg, expected", [ ["1", None], - ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["1000", (4, 1000, 1, 1, 1)], + ["1000-01", (2, 1000, 1, 1, 2)], + ["1000-01-01", (1, 1000, 1, 1, 3)], ["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.""" +def test_parse_iso_format(arg, expected): + """Returns an ``ISOFormat`` when given a valid ISO format string.""" - assert periods.parse_period(arg) == expected + assert periods.ISOFormat.parse(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 3ec05d5965..58bbe931d9 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -35,6 +35,9 @@ def test_offset(instant, offset, unit, expected): ["1000-01", periods.Instant((1000, 1, 1))], ["1000-01-01", periods.Instant((1000, 1, 1))], [1000, periods.Instant((1000, 1, 1))], + [(1000,), periods.Instant((1000, 1, 1))], + [(1000, 1), periods.Instant((1000, 1, 1))], + [(1000, 1, 1), periods.Instant((1000, 1, 1))], [datetime.date(1, 1, 1), periods.Instant((1, 1, 1))], [periods.Instant((1, 1, 1)), periods.Instant((1, 1, 1))], [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Instant((1, 1, 1))], @@ -49,6 +52,7 @@ def test_build_instant(arg, expected): ["1000-0", ValueError], ["1000-0-0", ValueError], ["1000-01-0", ValueError], + ["1000-01-01-01", ValueError], ["1000-01-1", ValueError], ["1000-01-32", ValueError], ["1000-1", ValueError], From 1211c7668079259d3e062acb7f8c53d447a2a9ab Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 11:59:50 +0100 Subject: [PATCH 59/93] Move the builder back into Period --- openfisca_core/periods/__init__.py | 1 - openfisca_core/periods/_parsers.py | 11 ++ openfisca_core/periods/helpers.py | 130 ------------------ openfisca_core/periods/period_.py | 126 ++++++++++++++++- openfisca_core/periods/tests/test__parsers.py | 59 -------- openfisca_core/periods/tests/test_period.py | 59 ++++++++ 6 files changed, 193 insertions(+), 193 deletions(-) delete mode 100644 openfisca_core/periods/helpers.py diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index f168b9609a..46643d692e 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -30,6 +30,5 @@ from ._config import INSTANT_PATTERN from ._parsers import ISOFormat from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR -from .helpers import build_period from .instant_ import Instant from .period_ import Period diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index 1f362ee6fd..bec70c51d0 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -8,10 +8,21 @@ class ISOFormat(NamedTuple): + """An implementation of the `parse` protocol.""" + + #: The unit of the parsed period, in binary. unit: int + + #: The year of the parsed period. year: int + + #: The month of the parsed period. month: int + + #: The month of the parsed period. day: int + + #: The number of fragments in the parsed period. shape: int @classmethod diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py deleted file mode 100644 index e58ed87eaf..0000000000 --- a/openfisca_core/periods/helpers.py +++ /dev/null @@ -1,130 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import datetime - -from ._errors import PeriodFormatError, PeriodTypeError -from ._parsers import ISOFormat -from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR -from .instant_ import Instant -from .period_ import Period - - -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((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(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 value in {ETERNITY, ETERNITY.name, ETERNITY.name.lower()}: - return Period((ETERNITY, Instant.build(datetime.date.min), 1)) - - if value is None or isinstance(value, DateUnit): - raise PeriodTypeError(value) - - if isinstance(value, Period): - return value - - if isinstance(value, Instant): - return Period((DAY, value, 1)) - - if isinstance(value, int): - return Period((YEAR, Instant((value, 1, 1)), 1)) - - if not isinstance(value, str): - raise PeriodFormatError(value) - - # Try to parse as a simple period - period = ISOFormat.parse(value) - - if period is not None: - return Period((DateUnit(period.unit), Instant((period[1:-1])), 1)) - - # Complex periods must have a ':' in their strings - if ":" not in value: - raise PeriodFormatError(value) - - # We know the first element has to be a ``unit`` - unit, *rest = value.split(":") - - # Units are case insensitive so we need to upper them - unit = unit.upper() - - # Left-most component must be a valid unit - if unit not in dir(DateUnit): - raise PeriodFormatError(value) - - unit = DateUnit[unit] - - # We get the first remaining component - period, *rest = rest - - # Middle component must be a valid ISO period - period = ISOFormat.parse(period) - - if period is None: - raise PeriodFormatError(value) - - # Finally we try to parse the size, if any - try: - size, *rest = rest - - except ValueError: - size = 1 - - # If provided, make sure the size is an integer - try: - size = int(size) - - except ValueError: - raise PeriodFormatError(value) - - # If there were more than 2 ":" in the string, the period is invalid - if len(rest) > 0: - raise PeriodFormatError(value) - - # Reject ambiguous periods such as month:2014 - if period.unit > unit: - raise PeriodFormatError(value) - - return Period((unit, Instant((period[1:-1])), size)) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 01648798c7..58bc11dd87 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,14 +1,15 @@ from __future__ import annotations -from typing import Callable, Sequence, Tuple +from typing import Any, Callable, Sequence, Tuple import datetime import inflect -from ._errors import DateUnitValueError +from ._errors import DateUnitValueError, PeriodFormatError, PeriodTypeError +from ._parsers import ISOFormat from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR -from .typing import Instant +from .instant_ import Instant class Period(Tuple[DateUnit, Instant, int]): @@ -559,3 +560,122 @@ def subperiods(self, unit: str) -> Sequence[Period]: self.this(unit).offset(offset, unit) for offset in range(self.count(unit)) ] + + @classmethod + def build(cls, 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: + >>> Period.build(Period((YEAR, Instant((2021, 1, 1)), 1))) + Period((year, Instant((2021, 1, 1)), 1)) + + >>> Period.build(Instant((2021, 1, 1))) + Period((day, Instant((2021, 1, 1)), 1)) + + >>> Period.build(ETERNITY) + Period((eternity, Instant((1, 1, 1)), 1)) + + >>> Period.build(2021) + Period((year, Instant((2021, 1, 1)), 1)) + + >>> Period.build("2014") + Period((year, Instant((2014, 1, 1)), 1)) + + >>> Period.build("year:2014") + Period((year, Instant((2014, 1, 1)), 1)) + + >>> Period.build("month:2014-02") + Period((month, Instant((2014, 2, 1)), 1)) + + >>> Period.build("year:2014-02") + Period((year, Instant((2014, 2, 1)), 1)) + + >>> Period.build("day:2014-02-02") + Period((day, Instant((2014, 2, 2)), 1)) + + >>> Period.build("day:2014-02-02:3") + Period((day, Instant((2014, 2, 2)), 3)) + + """ + + if value in {ETERNITY, ETERNITY.name, ETERNITY.name.lower()}: + return cls((ETERNITY, Instant.build(datetime.date.min), 1)) + + if value is None or isinstance(value, DateUnit): + raise PeriodTypeError(value) + + if isinstance(value, Period): + return value + + if isinstance(value, Instant): + return cls((DAY, value, 1)) + + if isinstance(value, int): + return cls((YEAR, Instant((value, 1, 1)), 1)) + + if not isinstance(value, str): + raise PeriodFormatError(value) + + # Try to parse as a simple period + period = ISOFormat.parse(value) + + if period is not None: + return cls((DateUnit(period.unit), Instant((period[1:-1])), 1)) + + # Complex periods must have a ':' in their strings + if ":" not in value: + raise PeriodFormatError(value) + + # We know the first element has to be a ``unit`` + unit, *rest = value.split(":") + + # Units are case insensitive so we need to upper them + unit = unit.upper() + + # Left-most component must be a valid unit + if unit not in dir(DateUnit): + raise PeriodFormatError(value) + + unit = DateUnit[unit] + + # We get the first remaining component + period, *rest = rest + + # Middle component must be a valid ISO period + period = ISOFormat.parse(period) + + if period is None: + raise PeriodFormatError(value) + + # Finally we try to parse the size, if any + try: + size, *rest = rest + + except ValueError: + size = 1 + + # If provided, make sure the size is an integer + try: + size = int(size) + + except ValueError: + raise PeriodFormatError(value) + + # If there were more than 2 ":" in the string, the period is invalid + if len(rest) > 0: + raise PeriodFormatError(value) + + # Reject ambiguous periods such as month:2014 + if period.unit > unit: + raise PeriodFormatError(value) + + return cls((unit, Instant((period[1:-1])), size)) diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test__parsers.py index 4faeed4d27..e5f4ebbb91 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test__parsers.py @@ -1,67 +1,8 @@ -import datetime - import pytest from openfisca_core import periods -@pytest.mark.parametrize("arg, expected", [ - ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], - ["1004-02-29", periods.Period((periods.DAY, periods.Instant((1004, 2, 29)), 1))], - ["ETERNITY", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], - ["day:1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], - ["day:1000-01-01:3", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 3))], - ["eternity", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], - ["month:1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["month:1000-01-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["month:1000-01-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], - ["month:1000-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], - ["year:1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["year:1000-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["year:1000-01-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["year:1000-01-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], - ["year:1000-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], - ["year:1000:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], - [1000, periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - [periods.ETERNITY, periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], - [periods.Instant((1, 1, 1)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 1))], - [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.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", [ - ["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], - ["day:1000-01", ValueError], - ["day:1000-01:1", ValueError], - ["day:1000:1", ValueError], - ["month:1000", ValueError], - ["month:1000:1", ValueError], - [None, TypeError], - [datetime.date(1, 1, 1), ValueError], - [periods.YEAR, TypeError], - ]) -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) - - @pytest.mark.parametrize("arg, expected", [ ["1", None], ["1000", (4, 1000, 1, 1, 1)], diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 7edcfbf793..6a471ac6d2 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -1,3 +1,5 @@ +import datetime + import pytest from openfisca_core import periods @@ -130,3 +132,60 @@ def test_day_size_in_days(date_unit, instant, size, expected): period = periods.Period((date_unit, instant, size)) assert period.count(periods.DAY) == expected + + +@pytest.mark.parametrize("arg, expected", [ + ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["1004-02-29", periods.Period((periods.DAY, periods.Instant((1004, 2, 29)), 1))], + ["ETERNITY", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], + ["day:1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], + ["day:1000-01-01:3", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 3))], + ["eternity", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], + ["month:1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["month:1000-01-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], + ["month:1000-01-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], + ["month:1000-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], + ["year:1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["year:1000-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["year:1000-01-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + ["year:1000-01-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], + ["year:1000-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], + ["year:1000:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], + [1000, periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], + [periods.ETERNITY, periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], + [periods.Instant((1, 1, 1)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 1))], + [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365))], + ]) +def test_build_period(arg, expected): + """Returns the expected ``Period``.""" + + assert periods.Period.build(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + ["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], + ["day:1000-01", ValueError], + ["day:1000-01:1", ValueError], + ["day:1000:1", ValueError], + ["month:1000", ValueError], + ["month:1000:1", ValueError], + [None, TypeError], + [datetime.date(1, 1, 1), ValueError], + [periods.YEAR, TypeError], + ]) +def test_build_period_with_an_invalid_argument(arg, error): + """Raises ``ValueError`` when given an invalid argument.""" + + with pytest.raises(error): + periods.Period.build(arg) From 54797753a233456a4afc432cad6e5cd29559157f Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 12:29:17 +0100 Subject: [PATCH 60/93] Replace build_period by Period.build --- .../data_storage/in_memory_storage.py | 12 +++---- .../data_storage/on_disk_storage.py | 14 ++++---- openfisca_core/holders/holder.py | 8 ++--- openfisca_core/model_api.py | 2 +- openfisca_core/parameters/parameter.py | 4 +-- openfisca_core/periods/tests/test_period.py | 4 +-- openfisca_core/populations/population.py | 2 +- .../scripts/measure_performances.py | 2 +- openfisca_core/simulations/simulation.py | 12 +++---- .../simulations/simulation_builder.py | 10 +++--- openfisca_core/variables/variable.py | 2 +- openfisca_web_api/loader/variables.py | 2 +- tests/core/test_countries.py | 2 +- tests/core/test_cycles.py | 2 +- tests/core/test_holders.py | 30 ++++++++-------- tests/core/test_opt_out_cache.py | 2 +- tests/core/test_reforms.py | 34 +++++++++---------- tests/core/variables/test_annualize.py | 8 ++--- 18 files changed, 76 insertions(+), 76 deletions(-) diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py index d5dc9e2f63..a6a96306bf 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.build_period(periods.ETERNITY) - period = periods.build_period(period) + period = periods.Period.build(periods.ETERNITY) + period = periods.Period.build(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.build_period(periods.ETERNITY) - period = periods.build_period(period) + period = periods.Period.build(periods.ETERNITY) + period = periods.Period.build(period) self._arrays[period] = value @@ -35,8 +35,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.build_period(periods.ETERNITY) - period = periods.build_period(period) + period = periods.Period.build(periods.ETERNITY) + period = periods.Period.build(period) self._arrays = { period_item: value diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py index f3ba18ebb3..d21e16c1ec 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -28,8 +28,8 @@ def _decode_file(self, file): def get(self, period): if self.is_eternal: - period = periods.build_period(periods.ETERNITY) - period = periods.build_period(period) + period = periods.Period.build(periods.ETERNITY) + period = periods.Period.build(period) values = self._files.get(period) if values is None: @@ -38,8 +38,8 @@ def get(self, period): def put(self, value, period): if self.is_eternal: - period = periods.build_period(periods.ETERNITY) - period = periods.build_period(period) + period = periods.Period.build(periods.ETERNITY) + period = periods.Period.build(period) filename = str(period) path = os.path.join(self.storage_dir, filename) + '.npy' @@ -55,8 +55,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.build_period(periods.ETERNITY) - period = periods.build_period(period) + period = periods.Period.build(periods.ETERNITY) + period = periods.Period.build(period) if period is not None: self._files = { @@ -76,7 +76,7 @@ def restore(self): continue path = os.path.join(self.storage_dir, filename) filename_core = filename.rsplit('.', 1)[0] - period = periods.build_period(filename_core) + period = periods.Period.build(filename_core) files[period] = path def __del__(self): diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index 8d29106acf..69d9b939d6 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -207,7 +207,7 @@ def set_input( """ - period = periods.build_period(period) + period = periods.Period.build(period) if period is None: raise ValueError(f"Invalid period value: {period}") @@ -218,7 +218,7 @@ def set_input( '{0} is only defined for {1}s. Please adapt your input.', ]).format( self.variable.name, - self.variable.definition_period + str(self.variable.definition_period) ) raise errors.PeriodMismatchError( self.variable.name, @@ -266,10 +266,10 @@ def _set(self, period, value): raise ValueError('A period must be specified to set values, except for variables with periods.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' + period_size_adj = f'{str(period.unit)}' if (period.size == 1) else f'{period.size}-{str(period.unit)}s' error_message = os.linesep.join([ f'Unable to set a value for variable "{name}" for {period_size_adj}-long period "{period}".', - f'"{name}" can only be set for one {self.variable.definition_period} at a time. Please adapt your input.', + f'"{name}" can only be set for one {str(self.variable.definition_period)} at a time. Please adapt your input.', f'If you are the maintainer of "{name}", you can consider adding it a set_input attribute to enable automatic period casting.' ]) diff --git a/openfisca_core/model_api.py b/openfisca_core/model_api.py index 8e306026d3..a2726cf15f 100644 --- a/openfisca_core/model_api.py +++ b/openfisca_core/model_api.py @@ -27,7 +27,7 @@ ValuesHistory, ) -from openfisca_core.periods import DAY, MONTH, YEAR, ETERNITY, build_period # noqa: F401 +from openfisca_core.periods import DAY, MONTH, YEAR, ETERNITY, Period # noqa: F401 from openfisca_core.populations import ADD, DIVIDE # noqa: F401 from openfisca_core.reforms import Reform # noqa: F401 diff --git a/openfisca_core/parameters/parameter.py b/openfisca_core/parameters/parameter.py index 63090bb3a6..14621ac42a 100644 --- a/openfisca_core/parameters/parameter.py +++ b/openfisca_core/parameters/parameter.py @@ -120,13 +120,13 @@ 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.build_period(period) + period = periods.Period.build(period) start = period.start stop = period.stop if start is None: raise ValueError("You must provide either a start or a period") start_str = str(start) - stop_str = str(stop.offset(1, 'day')) if stop else None + stop_str = str(stop.offset(1, periods.DAY)) if stop else None old_values = self.values_list new_values = [] diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 6a471ac6d2..ba2207881b 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -158,7 +158,7 @@ def test_day_size_in_days(date_unit, instant, size, expected): [periods.Instant((1, 1, 1)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 1))], [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365))], ]) -def test_build_period(arg, expected): +def test_build(arg, expected): """Returns the expected ``Period``.""" assert periods.Period.build(arg) == expected @@ -184,7 +184,7 @@ def test_build_period(arg, expected): [datetime.date(1, 1, 1), ValueError], [periods.YEAR, TypeError], ]) -def test_build_period_with_an_invalid_argument(arg, error): +def test_build_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" with pytest.raises(error): diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index 76b4ecb516..37fd10f301 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -112,7 +112,7 @@ def __call__( calculate: Calculate = Calculate( variable = variable_name, - period = periods.build_period(period), + period = periods.Period.build(period), option = options, ) diff --git a/openfisca_core/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index 341e61e47e..a9f71f3b3c 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -186,7 +186,7 @@ def formula(self, simulation, period): @timeit def check_revenu_disponible(year, city_code, expected_revenu_disponible): - simulation = simulations.Simulation(period = periods.build_period(year), tax_benefit_system = tax_benefit_system) + simulation = simulations.Simulation(period = periods.Period.build(year), tax_benefit_system = tax_benefit_system) famille = simulation.populations["famille"] famille.count = 3 famille.roles_count = 2 diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 26eeb51a2a..854a54f696 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -96,7 +96,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.build_period(period) + period = periods.Period.build(period) self.tracer.record_calculation_start(variable_name, period) @@ -168,10 +168,10 @@ 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.build_period(period) + period = periods.Period.build(period) # Check that the requested period matches definition_period - if periods.DateUnit[variable.definition_period] > periods.DateUnit[period.unit]: + if variable.definition_period > 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, @@ -197,7 +197,7 @@ 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.build_period(period) + period = periods.Period.build(period) # Check that the requested period matches definition_period if variable.definition_period != periods.YEAR: @@ -345,7 +345,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.build_period(period) + period = periods.Period.build(period) return self.get_holder(variable_name).get_array(period) def get_holder(self, variable_name: str): @@ -438,7 +438,7 @@ def set_input(self, variable_name: str, period, value): if variable is None: raise VariableNotFoundError(variable_name, self.tax_benefit_system) - period = periods.build_period(period) + period = periods.Period.build(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 e745573190..f6f0828205 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -325,7 +325,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.build_period(period_str)) + self.default_period = str(periods.Period.build(period_str)) def get_input(self, variable, period_str): if variable not in self.input_buffer: @@ -368,7 +368,7 @@ def init_variable_values(self, entity, instance_object, instance_id): for period_str, value in variable_values.items(): try: - periods.build_period(period_str) + periods.Period.build(period_str) except ValueError as e: raise SituationParsingError(path_in_json, e.args[0]) variable = entity.get_variable(variable_name) @@ -393,7 +393,7 @@ def add_variable_value(self, entity, variable, instance_index, instance_id, peri array[instance_index] = value - self.input_buffer[variable.name][str(periods.build_period(period_str))] = array + self.input_buffer[variable.name][str(periods.Period.build(period_str))] = array def finalize_variables_init(self, population): # Due to set_input mechanism, we must bufferize all inputs, then actually set them, @@ -411,9 +411,9 @@ 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.build_period(period_str) for period_str in self.input_buffer[variable_name].keys()] + unsorted_periods = [periods.Period.build(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 = lambda period: f"{periods.DateUnit[period.unit].value}_{period.size}") + sorted_periods = sorted(unsorted_periods, key = lambda period: f"{period.unit}_{period.size}") for period_value in sorted_periods: values = buffer[str(period_value)] diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index ac52329ca2..44bb2ddb1d 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -340,7 +340,7 @@ def get_formula( else: try: - instant = periods.build_period(period).start + instant = periods.Period.build(period).start except ValueError: instant = periods.Instant.build(period) diff --git a/openfisca_web_api/loader/variables.py b/openfisca_web_api/loader/variables.py index d9390fb3a2..35d982fa00 100644 --- a/openfisca_web_api/loader/variables.py +++ b/openfisca_web_api/loader/variables.py @@ -71,7 +71,7 @@ def build_variable(variable, country_package_metadata, tax_benefit_system): 'description': variable.label, 'valueType': VALUE_TYPES[variable.value_type]['formatted_value_type'], 'defaultValue': get_default_value(variable), - 'definitionPeriod': variable.definition_period.upper(), + 'definitionPeriod': variable.definition_period.__str__().upper(), 'entity': variable.entity.key, } diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index f5d21db260..f0aa2a4601 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -5,7 +5,7 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable -PERIOD = periods.build_period("2016-01") +PERIOD = periods.Period.build("2016-01") @pytest.mark.parametrize("simulation", [({"salary": 2000}, PERIOD)], indirect = True) diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index 35044de5c5..8ef363a3b1 100644 --- a/tests/core/test_cycles.py +++ b/tests/core/test_cycles.py @@ -10,7 +10,7 @@ @pytest.fixture def reference_period(): - return periods.build_period('2013-01') + return periods.Period.build('2013-01') @pytest.fixture diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index 928683adcb..22687e65fb 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -26,7 +26,7 @@ def couple(tax_benefit_system): build_from_entities(tax_benefit_system, situation_examples.couple) -period = periods.build_period('2017-12') +period = periods.Period.build('2017-12') def test_set_input_enum_string(couple): @@ -89,7 +89,7 @@ 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.build_period(periods.ETERNITY), value) + holder.set_input(periods.Period.build(periods.ETERNITY), value) assert holder.get_array(None) == value assert holder.get_array(periods.ETERNITY) == value assert holder.get_array('2016-01') == value @@ -98,8 +98,8 @@ def test_permanent_variable_filled(single): def test_delete_arrays(single): simulation = single salary_holder = simulation.person.get_holder('salary') - salary_holder.set_input(periods.build_period(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.build_period(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.Period.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.Period.build(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 +109,7 @@ def test_delete_arrays(single): salary_array = simulation.get_array('salary', '2018-01') assert salary_array is None - salary_holder.set_input(periods.build_period(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.Period.build(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -119,7 +119,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.build_period(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.Period.build(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 +132,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.build_period(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.Period.build(2017), numpy.asarray([30000])) simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-02') @@ -147,7 +147,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.build_period(2019), 'owner') + holders.set_input_dispatch_by_period(holder, periods.Period.build(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 +159,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.build_period(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.build_period(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.Period.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.Period.build(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.build_period(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.Period.build(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -172,7 +172,7 @@ def test_delete_arrays_on_disk(single): def test_cache_disk(couple): simulation = couple simulation.memory_config = force_storage_on_disk - month = periods.build_period('2017-01') + month = periods.Period.build('2017-01') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -183,8 +183,8 @@ def test_cache_disk(couple): def test_known_periods(couple): simulation = couple simulation.memory_config = force_storage_on_disk - month = periods.build_period('2017-01') - month_2 = periods.build_period('2017-02') + month = periods.Period.build('2017-01') + month_2 = periods.Period.build('2017-02') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -196,7 +196,7 @@ def test_known_periods(couple): def test_cache_enum_on_disk(single): simulation = single simulation.memory_config = force_storage_on_disk - month = periods.build_period('2017-01') + month = periods.Period.build('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 a3a2cf7a31..92c97d4776 100644 --- a/tests/core/test_opt_out_cache.py +++ b/tests/core/test_opt_out_cache.py @@ -7,7 +7,7 @@ from openfisca_core.variables import Variable -PERIOD = periods.build_period("2016-01") +PERIOD = periods.Period.build("2016-01") class input(Variable): diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index 85b54abadf..197ff50eaa 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -124,23 +124,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.build_period(2013).start, - periods.build_period(2013).stop, + periods.Period.build(2013).start, + periods.Period.build(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.build_period(2015).start, - periods.build_period(2015).stop, + periods.Period.build(2015).start, + periods.Period.build(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.build_period(2013).start, + periods.Period.build(2013).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2013-01-01": {'value': 1.0}}), @@ -148,15 +148,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.build_period(2011).start, - periods.build_period(2011).stop, + periods.Period.build(2011).start, + periods.Period.build(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.build_period(2015).start, + periods.Period.build(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 +164,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.build_period(2014).start, - periods.build_period(2014).stop, + periods.Period.build(2014).start, + periods.Period.build(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.build_period(2014).start, + periods.Period.build(2014).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 1.0}}), @@ -180,23 +180,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.build_period(2005).start, - periods.build_period(2005).stop, + periods.Period.build(2005).start, + periods.Period.build(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.build_period(2003).start, - periods.build_period(2003).stop, + periods.Period.build(2003).start, + periods.Period.build(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.build_period(2005).start, + periods.Period.build(2005).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2005-01-01": {'value': 1.0}}), @@ -204,7 +204,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.build_period(2006).start, + periods.Period.build(2006).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 1.0}}), diff --git a/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index c1cb1bbb3f..38b242bd02 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -41,7 +41,7 @@ def __call__(self, variable_name: str, period): def test_without_annualize(monthly_variable): - period = periods.build_period(2019) + period = periods.Period.build(2019) person = PopulationMock(monthly_variable) @@ -55,7 +55,7 @@ def test_without_annualize(monthly_variable): def test_with_annualize(monthly_variable): - period = periods.build_period(2019) + period = periods.Period.build(2019) annualized_variable = get_annualized_variable(monthly_variable) person = PopulationMock(annualized_variable) @@ -70,8 +70,8 @@ def test_with_annualize(monthly_variable): def test_with_partial_annualize(monthly_variable): - period = periods.build_period('year:2018:2') - annualized_variable = get_annualized_variable(monthly_variable, periods.build_period(2018)) + period = periods.Period.build('year:2018:2') + annualized_variable = get_annualized_variable(monthly_variable, periods.Period.build(2018)) person = PopulationMock(annualized_variable) From 7adcac6eda4609c3e747875367da17a600327f9d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 13:32:22 +0100 Subject: [PATCH 61/93] Adapt public exposed API --- .../data_storage/in_memory_storage.py | 12 +- .../data_storage/on_disk_storage.py | 14 +- openfisca_core/holders/helpers.py | 7 +- openfisca_core/holders/holder.py | 6 +- openfisca_core/holders/tests/test_helpers.py | 9 +- openfisca_core/model_api.py | 31 ++- openfisca_core/parameters/at_instant_like.py | 2 +- openfisca_core/parameters/parameter.py | 2 +- openfisca_core/periods/__init__.py | 16 +- openfisca_core/periods/instant_.py | 16 +- openfisca_core/periods/period_.py | 75 +++----- openfisca_core/periods/tests/test__parsers.py | 2 +- openfisca_core/periods/tests/test_instant.py | 46 ++--- openfisca_core/periods/tests/test_period.py | 178 +++++++++--------- openfisca_core/populations/population.py | 2 +- .../scripts/measure_performances.py | 2 +- openfisca_core/simulations/simulation.py | 26 ++- .../simulations/simulation_builder.py | 8 +- .../taxbenefitsystems/tax_benefit_system.py | 2 +- openfisca_core/variables/variable.py | 9 +- .../test_marginal_amount_tax_scale.py | 2 +- .../test_single_amount_tax_scale.py | 6 +- tests/core/test_countries.py | 2 +- tests/core/test_cycles.py | 2 +- tests/core/test_holders.py | 30 +-- tests/core/test_opt_out_cache.py | 2 +- tests/core/test_projectors.py | 22 ++- tests/core/test_reforms.py | 56 +++--- tests/core/variables/test_annualize.py | 16 +- 29 files changed, 293 insertions(+), 310 deletions(-) diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py index a6a96306bf..5b479474ee 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.build(periods.ETERNITY) - period = periods.Period.build(period) + period = periods.build(periods.ETERNITY) + period = periods.build(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.build(periods.ETERNITY) - period = periods.Period.build(period) + period = periods.build(periods.ETERNITY) + period = periods.build(period) self._arrays[period] = value @@ -35,8 +35,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.Period.build(periods.ETERNITY) - period = periods.Period.build(period) + period = periods.build(periods.ETERNITY) + period = periods.build(period) self._arrays = { period_item: value diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py index d21e16c1ec..cfcab32cc1 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -28,8 +28,8 @@ def _decode_file(self, file): def get(self, period): if self.is_eternal: - period = periods.Period.build(periods.ETERNITY) - period = periods.Period.build(period) + period = periods.build(periods.ETERNITY) + period = periods.build(period) values = self._files.get(period) if values is None: @@ -38,8 +38,8 @@ def get(self, period): def put(self, value, period): if self.is_eternal: - period = periods.Period.build(periods.ETERNITY) - period = periods.Period.build(period) + period = periods.build(periods.ETERNITY) + period = periods.build(period) filename = str(period) path = os.path.join(self.storage_dir, filename) + '.npy' @@ -55,8 +55,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.Period.build(periods.ETERNITY) - period = periods.Period.build(period) + period = periods.build(periods.ETERNITY) + period = periods.build(period) if period is not None: self._files = { @@ -76,7 +76,7 @@ def restore(self): continue path = os.path.join(self.storage_dir, filename) filename_core = filename.rsplit('.', 1)[0] - period = periods.Period.build(filename_core) + period = periods.build(filename_core) files[period] = path def __del__(self): diff --git a/openfisca_core/holders/helpers.py b/openfisca_core/holders/helpers.py index 176f6b6f30..e9cd9b06b3 100644 --- a/openfisca_core/holders/helpers.py +++ b/openfisca_core/holders/helpers.py @@ -3,7 +3,6 @@ import numpy from openfisca_core import periods -from openfisca_core.periods import Period log = logging.getLogger(__name__) @@ -28,7 +27,7 @@ def set_input_dispatch_by_period(holder, period, array): after_instant = period.start.offset(period_size, period_unit) # Cache the input data, skipping the existing cached months - sub_period = Period((cached_period_unit, period.start, 1)) + sub_period = periods.period((cached_period_unit, period.start, 1)) while sub_period.start < after_instant: existing_array = holder.get_array(sub_period) if existing_array is None: @@ -61,7 +60,7 @@ def set_input_divide_by_period(holder, period, array): # Count the number of elementary periods to change, and the difference with what is already known. remaining_array = array.copy() - sub_period = Period((cached_period_unit, period.start, 1)) + sub_period = periods.period((cached_period_unit, period.start, 1)) sub_periods_count = 0 while sub_period.start < after_instant: existing_array = holder.get_array(sub_period) @@ -74,7 +73,7 @@ def set_input_divide_by_period(holder, period, array): # Cache the input data if sub_periods_count > 0: divided_array = remaining_array / sub_periods_count - sub_period = Period((cached_period_unit, period.start, 1)) + sub_period = periods.period((cached_period_unit, period.start, 1)) while sub_period.start < after_instant: if holder.get_array(sub_period) is None: holder._set(sub_period, divided_array) diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index 69d9b939d6..c734625d85 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -113,7 +113,7 @@ def get_memory_usage(self) -> MemoryUsage: >>> entity = entities.Entity("", "", "", "") >>> class MyVariable(variables.Variable): - ... definition_period = "year" + ... definition_period = periods.YEAR ... entity = entity ... value_type = int @@ -184,7 +184,7 @@ def set_input( >>> entity = entities.Entity("", "", "", "") >>> class MyVariable(variables.Variable): - ... definition_period = "year" + ... definition_period = periods.YEAR ... entity = entity ... value_type = int @@ -207,7 +207,7 @@ def set_input( """ - period = periods.Period.build(period) + period = periods.build(period) if period is None: raise ValueError(f"Invalid period value: {period}") diff --git a/openfisca_core/holders/tests/test_helpers.py b/openfisca_core/holders/tests/test_helpers.py index 6ba1e7a815..6574c2f008 100644 --- a/openfisca_core/holders/tests/test_helpers.py +++ b/openfisca_core/holders/tests/test_helpers.py @@ -3,7 +3,6 @@ from openfisca_core import holders, periods, tools from openfisca_core.entities import Entity from openfisca_core.holders import Holder -from openfisca_core.periods import Instant, Period from openfisca_core.populations import Population from openfisca_core.variables import Variable @@ -58,8 +57,8 @@ def test_set_input_dispatch_by_period( Income.definition_period = definition_unit income = Income() holder = Holder(income, population) - instant = Instant((2022, 1, 1)) - dispatch_period = Period((dispatch_unit, instant, 3)) + instant = periods.instant((2022, 1, 1)) + dispatch_period = periods.period((dispatch_unit, instant, 3)) holders.set_input_dispatch_by_period(holder, dispatch_period, values) total = sum(map(holder.get_array, holder.get_known_periods())) @@ -89,8 +88,8 @@ def test_set_input_divide_by_period( Income.definition_period = definition_unit income = Income() holder = Holder(income, population) - instant = Instant((2022, 1, 1)) - divide_period = Period((divide_unit, instant, 3)) + instant = periods.instant((2022, 1, 1)) + divide_period = periods.period((divide_unit, instant, 3)) holders.set_input_divide_by_period(holder, divide_period, values) last = holder.get_array(holder.get_known_periods()[-1]) diff --git a/openfisca_core/model_api.py b/openfisca_core/model_api.py index a2726cf15f..3c126215b6 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 import periods # 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.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/at_instant_like.py b/openfisca_core/parameters/at_instant_like.py index 2626fcf067..965348b77f 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.build(instant)) + instant = str(periods.instant.build(instant)) return self._get_at_instant(instant) @abc.abstractmethod diff --git a/openfisca_core/parameters/parameter.py b/openfisca_core/parameters/parameter.py index 14621ac42a..8c038c611b 100644 --- a/openfisca_core/parameters/parameter.py +++ b/openfisca_core/parameters/parameter.py @@ -120,7 +120,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.build(period) + period = periods.build(period) start = period.start stop = period.stop if start is None: diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 46643d692e..373d3afcb8 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -27,8 +27,16 @@ """ +from . import _parsers as parsers +from . import _units as units from ._config import INSTANT_PATTERN -from ._parsers import ISOFormat -from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR -from .instant_ import Instant -from .period_ import Period +from .instant_ import Instant as instant +from .period_ import Period as period + +DAY = units.DAY +MONTH = units.MONTH +YEAR = units.YEAR +ETERNITY = units.ETERNITY +dateunit = units.DateUnit +build = period.build +isoformat = parsers.ISOFormat.parse diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 71ae0356be..d6e1fcfafa 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -227,24 +227,24 @@ def build(cls, value: Any) -> Instant: Examples: >>> from openfisca_core import periods - >>> periods.Instant.build(datetime.date(2021, 9, 16)) + >>> Instant.build(datetime.date(2021, 9, 16)) Instant((2021, 9, 16)) - >>> periods.Instant.build(Instant((2021, 9, 16))) + >>> Instant.build(Instant((2021, 9, 16))) Instant((2021, 9, 16)) - >>> periods.Instant.build("2021") + >>> Instant.build("2021") Instant((2021, 1, 1)) - >>> periods.Instant.build(2021) + >>> Instant.build(2021) Instant((2021, 1, 1)) - >>> periods.Instant.build((2021, 9)) + >>> Instant.build((2021, 9)) Instant((2021, 9, 1)) - >>> start = periods.Instant((2021, 9, 16)) - >>> period = periods.Period((YEAR, start, 1)) - >>> periods.Instant.build(period) + >>> start = Instant((2021, 9, 16)) + >>> period = periods.period((YEAR, start, 1)) + >>> Instant.build(period) Instant((2021, 9, 16)) .. versionadded:: 39.0.0 diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 58bc11dd87..e8e3d6e275 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -9,7 +9,8 @@ from ._errors import DateUnitValueError, PeriodFormatError, PeriodTypeError from ._parsers import ISOFormat from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR -from .instant_ import Instant +from .instant_ import Instant as instant +from .typing import Instant class Period(Tuple[DateUnit, Instant, int]): @@ -30,9 +31,7 @@ class Period(Tuple[DateUnit, Instant, int]): The ``unit``, ``start``, and ``size``, accordingly. Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 9, 1)) + >>> start = instant((2021, 9, 1)) >>> period = Period((YEAR, start, 3)) ``Periods`` are represented as a ``tuple`` containing the ``unit``, @@ -92,9 +91,7 @@ def __str__(self) -> str: str: A string representation of the period. Examples: - >>> from openfisca_core import periods - - >>> jan = periods.Instant((2021, 1, 1)) + >>> jan = instant((2021, 1, 1)) >>> feb = jan.offset(1, MONTH) >>> str(Period((YEAR, jan, 1))) @@ -162,9 +159,7 @@ def __contains__(self, other: object) -> bool: True if ``other`` is contained, otherwise False. Example: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 1, 1)) + >>> start = instant((2021, 1, 1)) >>> period = Period((YEAR, start, 1)) >>> sub_period = Period((MONTH, start, 3)) >>> sub_period in period @@ -185,9 +180,7 @@ def unit(self) -> str: An int. Example: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 10, 1)) + >>> start = instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) >>> period.unit year @@ -204,9 +197,7 @@ def start(self) -> Instant: An Instant. Example: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 10, 1)) + >>> start = instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) >>> period.start Instant((2021, 10, 1)) @@ -223,9 +214,7 @@ def size(self) -> int: An int. Example: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 10, 1)) + >>> start = instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) >>> period.size 3 @@ -245,9 +234,7 @@ def stop(self) -> Instant: DateUnitValueError: If the period's unit isn't day, month or year. Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2012, 2, 29)) + >>> start = instant((2012, 2, 29)) >>> Period((YEAR, start, 2)).stop Instant((2014, 2, 27)) @@ -284,9 +271,7 @@ def date(self) -> datetime.date: ValueError: If the period's size is greater than 1. Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 10, 1)) + >>> start = instant((2021, 10, 1)) >>> period = Period((YEAR, start, 1)) >>> period.date() @@ -320,9 +305,7 @@ def count(self, unit: str) -> int: ValueError: If the period's unit is not a day, a month or a year. Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 10, 1)) + >>> start = instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) >>> period.count(DAY) @@ -365,7 +348,7 @@ def count(self, unit: str) -> int: raise ValueError( f"Cannot calculate number of {self.plural(unit)} in a " - f"{self.unit}." + f"{str(self.unit)}." ) def this(self, unit: str) -> Period: @@ -378,9 +361,7 @@ def this(self, unit: str) -> Period: A Period. Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2023, 1, 1)) + >>> start = instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -410,9 +391,7 @@ def last(self, unit: str, size: int = 1) -> Period: A Period. Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2023, 1, 1)) + >>> start = instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -451,9 +430,7 @@ def ago(self, unit: str, size: int = 1) -> Period: A Period. Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2023, 1, 1)) + >>> start = instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -492,9 +469,7 @@ def offset(self, offset: str | int, unit: str | None = None) -> Period: Period: A new one. Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2014, 2, 3)) + >>> start = instant((2014, 2, 3)) >>> Period((DAY, start, 1)).offset("first-of", MONTH) Period((day, Instant((2014, 2, 1)), 1)) @@ -502,7 +477,7 @@ def offset(self, offset: str | int, unit: str | None = None) -> Period: >>> Period((MONTH, start, 4)).offset("last-of", MONTH) Period((month, Instant((2014, 2, 28)), 4)) - >>> start = periods.Instant((2021, 1, 1)) + >>> start = instant((2021, 1, 1)) >>> Period((DAY, start, 365)).offset(-3) Period((day, Instant((2020, 12, 29)), 365)) @@ -533,9 +508,7 @@ def subperiods(self, unit: str) -> Sequence[Period]: ValueError: If the period's unit is smaller than the given unit. Examples: - >>> from openfisca_core import periods - - >>> start = periods.Instant((2021, 1, 1)) + >>> start = instant((2021, 1, 1)) >>> period = Period((YEAR, start, 1)) >>> period.subperiods(MONTH) @@ -575,10 +548,10 @@ def build(cls, value: Any) -> Period: PeriodFormatError: When the arguments were invalid, like "2021-32-13". Examples: - >>> Period.build(Period((YEAR, Instant((2021, 1, 1)), 1))) + >>> Period.build(Period((YEAR, instant((2021, 1, 1)), 1))) Period((year, Instant((2021, 1, 1)), 1)) - >>> Period.build(Instant((2021, 1, 1))) + >>> Period.build(instant((2021, 1, 1))) Period((day, Instant((2021, 1, 1)), 1)) >>> Period.build(ETERNITY) @@ -608,7 +581,7 @@ def build(cls, value: Any) -> Period: """ if value in {ETERNITY, ETERNITY.name, ETERNITY.name.lower()}: - return cls((ETERNITY, Instant.build(datetime.date.min), 1)) + return cls((ETERNITY, instant.build(datetime.date.min), 1)) if value is None or isinstance(value, DateUnit): raise PeriodTypeError(value) @@ -620,7 +593,7 @@ def build(cls, value: Any) -> Period: return cls((DAY, value, 1)) if isinstance(value, int): - return cls((YEAR, Instant((value, 1, 1)), 1)) + return cls((YEAR, instant((value, 1, 1)), 1)) if not isinstance(value, str): raise PeriodFormatError(value) @@ -629,7 +602,7 @@ def build(cls, value: Any) -> Period: period = ISOFormat.parse(value) if period is not None: - return cls((DateUnit(period.unit), Instant((period[1:-1])), 1)) + return cls((DateUnit(period.unit), instant((period[1:-1])), 1)) # Complex periods must have a ':' in their strings if ":" not in value: @@ -678,4 +651,4 @@ def build(cls, value: Any) -> Period: if period.unit > unit: raise PeriodFormatError(value) - return cls((unit, Instant((period[1:-1])), size)) + return cls((unit, instant((period[1:-1])), size)) diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test__parsers.py index e5f4ebbb91..428ac61a19 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test__parsers.py @@ -17,4 +17,4 @@ def test_parse_iso_format(arg, expected): """Returns an ``ISOFormat`` when given a valid ISO format string.""" - assert periods.ISOFormat.parse(arg) == expected + assert periods.isoformat(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 58bbe931d9..dabecca5b4 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -9,20 +9,20 @@ def instant(): """Returns a ``Instant``.""" - return periods.Instant((2020, 2, 29)) + return periods.instant((2020, 2, 29)) @pytest.mark.parametrize("offset, unit, expected", [ - ["first-of", periods.MONTH, periods.Instant((2020, 2, 1))], - ["first-of", periods.YEAR, periods.Instant((2020, 1, 1))], - ["last-of", periods.MONTH, periods.Instant((2020, 2, 29))], - ["last-of", periods.YEAR, periods.Instant((2020, 12, 31))], - [-3, periods.DAY, periods.Instant((2020, 2, 26))], - [-3, periods.MONTH, periods.Instant((2019, 11, 29))], - [-3, periods.YEAR, periods.Instant((2017, 2, 28))], - [3, periods.DAY, periods.Instant((2020, 3, 3))], - [3, periods.MONTH, periods.Instant((2020, 5, 29))], - [3, periods.YEAR, periods.Instant((2023, 2, 28))], + ["first-of", periods.MONTH, periods.instant((2020, 2, 1))], + ["first-of", periods.YEAR, periods.instant((2020, 1, 1))], + ["last-of", periods.MONTH, periods.instant((2020, 2, 29))], + ["last-of", periods.YEAR, periods.instant((2020, 12, 31))], + [-3, periods.DAY, periods.instant((2020, 2, 26))], + [-3, periods.MONTH, periods.instant((2019, 11, 29))], + [-3, periods.YEAR, periods.instant((2017, 2, 28))], + [3, periods.DAY, periods.instant((2020, 3, 3))], + [3, periods.MONTH, periods.instant((2020, 5, 29))], + [3, periods.YEAR, periods.instant((2023, 2, 28))], ]) def test_offset(instant, offset, unit, expected): """Returns the expected ``Instant``.""" @@ -31,21 +31,21 @@ def test_offset(instant, offset, unit, expected): @pytest.mark.parametrize("arg, expected", [ - ["1000", periods.Instant((1000, 1, 1))], - ["1000-01", periods.Instant((1000, 1, 1))], - ["1000-01-01", periods.Instant((1000, 1, 1))], - [1000, periods.Instant((1000, 1, 1))], - [(1000,), periods.Instant((1000, 1, 1))], - [(1000, 1), periods.Instant((1000, 1, 1))], - [(1000, 1, 1), periods.Instant((1000, 1, 1))], - [datetime.date(1, 1, 1), periods.Instant((1, 1, 1))], - [periods.Instant((1, 1, 1)), periods.Instant((1, 1, 1))], - [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Instant((1, 1, 1))], + ["1000", periods.instant((1000, 1, 1))], + ["1000-01", periods.instant((1000, 1, 1))], + ["1000-01-01", periods.instant((1000, 1, 1))], + [1000, periods.instant((1000, 1, 1))], + [(1000,), periods.instant((1000, 1, 1))], + [(1000, 1), periods.instant((1000, 1, 1))], + [(1000, 1, 1), periods.instant((1000, 1, 1))], + [datetime.date(1, 1, 1), periods.instant((1, 1, 1))], + [periods.instant((1, 1, 1)), periods.instant((1, 1, 1))], + [periods.period((periods.DAY, periods.instant((1, 1, 1)), 365)), periods.instant((1, 1, 1))], ]) def test_build_instant(arg, expected): """Returns the expected ``Instant``.""" - assert periods.Instant.build(arg) == expected + assert periods.instant.build(arg) == expected @pytest.mark.parametrize("arg, error", [ @@ -71,4 +71,4 @@ def test_build_instant_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" with pytest.raises(error): - periods.Instant.build(arg) + periods.instant.build(arg) diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index ba2207881b..be92e430a3 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -9,159 +9,159 @@ def instant(): """Returns a ``Instant``.""" - return periods.Instant((2022, 12, 31)) + return periods.instant((2022, 12, 31)) @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, periods.Instant((2022, 1, 1)), 12, "2022"], - [periods.MONTH, periods.Instant((2022, 3, 1)), 12, "year:2022-03"], - [periods.YEAR, periods.Instant((2022, 1, 1)), 1, "2022"], - [periods.YEAR, periods.Instant((2022, 1, 1)), 3, "year:2022:3"], - [periods.YEAR, periods.Instant((2022, 1, 3)), 3, "year:2022:3"], - [periods.YEAR, periods.Instant((2022, 3, 1)), 1, "year:2022-03"], + [periods.MONTH, periods.instant((2022, 1, 1)), 12, "2022"], + [periods.MONTH, periods.instant((2022, 3, 1)), 12, "year:2022-03"], + [periods.YEAR, periods.instant((2022, 1, 1)), 1, "2022"], + [periods.YEAR, periods.instant((2022, 1, 1)), 3, "year:2022:3"], + [periods.YEAR, periods.instant((2022, 1, 3)), 3, "year:2022:3"], + [periods.YEAR, periods.instant((2022, 3, 1)), 1, "year:2022-03"], ]) def test_str_with_years(date_unit, instant, size, expected): """Returns the expected string.""" - assert str(periods.Period((date_unit, instant, size))) == expected + assert str(periods.period((date_unit, instant, size))) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, periods.Instant((2022, 1, 1)), 1, "2022-01"], - [periods.MONTH, periods.Instant((2022, 1, 1)), 3, "month:2022-01:3"], - [periods.MONTH, periods.Instant((2022, 3, 1)), 3, "month:2022-03:3"], + [periods.MONTH, periods.instant((2022, 1, 1)), 1, "2022-01"], + [periods.MONTH, periods.instant((2022, 1, 1)), 3, "month:2022-01:3"], + [periods.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 + assert str(periods.period((date_unit, instant, size))) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.DAY, periods.Instant((2022, 1, 1)), 1, "2022-01-01"], - [periods.DAY, periods.Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], - [periods.DAY, periods.Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], + [periods.DAY, periods.instant((2022, 1, 1)), 1, "2022-01-01"], + [periods.DAY, periods.instant((2022, 1, 1)), 3, "day:2022-01-01:3"], + [periods.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 + assert str(periods.period((date_unit, instant, size))) == expected @pytest.mark.parametrize("period_unit, unit, start, cease, count", [ - [periods.DAY, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2023, 1, 2)), 3], - [periods.MONTH, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2023, 3, 30)), 90], - [periods.MONTH, periods.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2023, 2, 1)), 3], - [periods.YEAR, periods.DAY, periods.Instant((2022, 12, 31)), periods.Instant((2025, 12, 30)), 1096], - [periods.YEAR, periods.MONTH, periods.Instant((2022, 12, 1)), periods.Instant((2025, 11, 1)), 36], - [periods.YEAR, periods.YEAR, periods.Instant((2022, 1, 1)), periods.Instant((2024, 1, 1)), 3], + [periods.DAY, periods.DAY, periods.instant((2022, 12, 31)), periods.instant((2023, 1, 2)), 3], + [periods.MONTH, periods.DAY, periods.instant((2022, 12, 31)), periods.instant((2023, 3, 30)), 90], + [periods.MONTH, periods.MONTH, periods.instant((2022, 12, 1)), periods.instant((2023, 2, 1)), 3], + [periods.YEAR, periods.DAY, periods.instant((2022, 12, 31)), periods.instant((2025, 12, 30)), 1096], + [periods.YEAR, periods.MONTH, periods.instant((2022, 12, 1)), periods.instant((2025, 11, 1)), 36], + [periods.YEAR, periods.YEAR, periods.instant((2022, 1, 1)), periods.instant((2024, 1, 1)), 3], ]) def test_subperiods(instant, period_unit, unit, start, cease, count): """Returns the expected subperiods.""" - period = periods.Period((period_unit, instant, 3)) + 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)) + 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.DAY, "first-of", periods.MONTH, periods.Period((periods.DAY, periods.Instant((2022, 12, 1)), 3))], - [periods.DAY, "first-of", periods.YEAR, periods.Period((periods.DAY, periods.Instant((2022, 1, 1)), 3))], - [periods.DAY, "last-of", periods.MONTH, periods.Period((periods.DAY, periods.Instant((2022, 12, 31)), 3))], - [periods.DAY, "last-of", periods.YEAR, periods.Period((periods.DAY, periods.Instant((2022, 12, 31)), 3))], - [periods.DAY, -3, periods.YEAR, periods.Period((periods.DAY, periods.Instant((2019, 12, 31)), 3))], - [periods.DAY, 1, periods.MONTH, periods.Period((periods.DAY, periods.Instant((2023, 1, 31)), 3))], - [periods.DAY, 3, periods.DAY, periods.Period((periods.DAY, periods.Instant((2023, 1, 3)), 3))], - [periods.MONTH, "first-of", periods.MONTH, periods.Period((periods.MONTH, periods.Instant((2022, 12, 1)), 3))], - [periods.MONTH, "first-of", periods.YEAR, periods.Period((periods.MONTH, periods.Instant((2022, 1, 1)), 3))], - [periods.MONTH, "last-of", periods.MONTH, periods.Period((periods.MONTH, periods.Instant((2022, 12, 31)), 3))], - [periods.MONTH, "last-of", periods.YEAR, periods.Period((periods.MONTH, periods.Instant((2022, 12, 31)), 3))], - [periods.MONTH, -3, periods.YEAR, periods.Period((periods.MONTH, periods.Instant((2019, 12, 31)), 3))], - [periods.MONTH, 1, periods.MONTH, periods.Period((periods.MONTH, periods.Instant((2023, 1, 31)), 3))], - [periods.MONTH, 3, periods.DAY, periods.Period((periods.MONTH, periods.Instant((2023, 1, 3)), 3))], - [periods.YEAR, "first-of", periods.MONTH, periods.Period((periods.YEAR, periods.Instant((2022, 12, 1)), 3))], - [periods.YEAR, "first-of", periods.YEAR, periods.Period((periods.YEAR, periods.Instant((2022, 1, 1)), 3))], - [periods.YEAR, "last-of", periods.MONTH, periods.Period((periods.YEAR, periods.Instant((2022, 12, 31)), 3))], - [periods.YEAR, "last-of", periods.YEAR, periods.Period((periods.YEAR, periods.Instant((2022, 12, 31)), 3))], - [periods.YEAR, -3, periods.YEAR, periods.Period((periods.YEAR, periods.Instant((2019, 12, 31)), 3))], - [periods.YEAR, 1, periods.MONTH, periods.Period((periods.YEAR, periods.Instant((2023, 1, 31)), 3))], - [periods.YEAR, 3, periods.DAY, periods.Period((periods.YEAR, periods.Instant((2023, 1, 3)), 3))], + [periods.DAY, "first-of", periods.MONTH, periods.period((periods.DAY, periods.instant((2022, 12, 1)), 3))], + [periods.DAY, "first-of", periods.YEAR, periods.period((periods.DAY, periods.instant((2022, 1, 1)), 3))], + [periods.DAY, "last-of", periods.MONTH, periods.period((periods.DAY, periods.instant((2022, 12, 31)), 3))], + [periods.DAY, "last-of", periods.YEAR, periods.period((periods.DAY, periods.instant((2022, 12, 31)), 3))], + [periods.DAY, -3, periods.YEAR, periods.period((periods.DAY, periods.instant((2019, 12, 31)), 3))], + [periods.DAY, 1, periods.MONTH, periods.period((periods.DAY, periods.instant((2023, 1, 31)), 3))], + [periods.DAY, 3, periods.DAY, periods.period((periods.DAY, periods.instant((2023, 1, 3)), 3))], + [periods.MONTH, "first-of", periods.MONTH, periods.period((periods.MONTH, periods.instant((2022, 12, 1)), 3))], + [periods.MONTH, "first-of", periods.YEAR, periods.period((periods.MONTH, periods.instant((2022, 1, 1)), 3))], + [periods.MONTH, "last-of", periods.MONTH, periods.period((periods.MONTH, periods.instant((2022, 12, 31)), 3))], + [periods.MONTH, "last-of", periods.YEAR, periods.period((periods.MONTH, periods.instant((2022, 12, 31)), 3))], + [periods.MONTH, -3, periods.YEAR, periods.period((periods.MONTH, periods.instant((2019, 12, 31)), 3))], + [periods.MONTH, 1, periods.MONTH, periods.period((periods.MONTH, periods.instant((2023, 1, 31)), 3))], + [periods.MONTH, 3, periods.DAY, periods.period((periods.MONTH, periods.instant((2023, 1, 3)), 3))], + [periods.YEAR, "first-of", periods.MONTH, periods.period((periods.YEAR, periods.instant((2022, 12, 1)), 3))], + [periods.YEAR, "first-of", periods.YEAR, periods.period((periods.YEAR, periods.instant((2022, 1, 1)), 3))], + [periods.YEAR, "last-of", periods.MONTH, periods.period((periods.YEAR, periods.instant((2022, 12, 31)), 3))], + [periods.YEAR, "last-of", periods.YEAR, periods.period((periods.YEAR, periods.instant((2022, 12, 31)), 3))], + [periods.YEAR, -3, periods.YEAR, periods.period((periods.YEAR, periods.instant((2019, 12, 31)), 3))], + [periods.YEAR, 1, periods.MONTH, periods.period((periods.YEAR, periods.instant((2023, 1, 31)), 3))], + [periods.YEAR, 3, periods.DAY, periods.period((periods.YEAR, 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)) + period = periods.period((period_unit, instant, 3)) assert period.offset(offset, unit) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, periods.Instant((2012, 1, 3)), 3, 3], - [periods.MONTH, periods.Instant((2012, 2, 3)), 1, 1], - [periods.MONTH, periods.Instant((2022, 1, 3)), 3, 3], - [periods.MONTH, periods.Instant((2022, 12, 1)), 1, 1], - [periods.YEAR, periods.Instant((2012, 1, 1)), 1, 12], - [periods.YEAR, periods.Instant((2022, 1, 1)), 2, 24], - [periods.YEAR, periods.Instant((2022, 12, 1)), 1, 12], + [periods.MONTH, periods.instant((2012, 1, 3)), 3, 3], + [periods.MONTH, periods.instant((2012, 2, 3)), 1, 1], + [periods.MONTH, periods.instant((2022, 1, 3)), 3, 3], + [periods.MONTH, periods.instant((2022, 12, 1)), 1, 1], + [periods.YEAR, periods.instant((2012, 1, 1)), 1, 12], + [periods.YEAR, periods.instant((2022, 1, 1)), 2, 24], + [periods.YEAR, periods.instant((2022, 12, 1)), 1, 12], ]) def test_day_size_in_months(date_unit, instant, size, expected): """Returns the expected number of months.""" - period = periods.Period((date_unit, instant, size)) + period = periods.period((date_unit, instant, size)) assert period.count(periods.MONTH) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.DAY, periods.Instant((2022, 12, 31)), 1, 1], - [periods.DAY, periods.Instant((2022, 12, 31)), 3, 3], - [periods.MONTH, periods.Instant((2012, 1, 3)), 3, 31 + 29 + 31], - [periods.MONTH, periods.Instant((2012, 2, 3)), 1, 29], - [periods.MONTH, periods.Instant((2022, 1, 3)), 3, 31 + 28 + 31], - [periods.MONTH, periods.Instant((2022, 12, 1)), 1, 31], - [periods.YEAR, periods.Instant((2012, 1, 1)), 1, 366], - [periods.YEAR, periods.Instant((2022, 1, 1)), 2, 730], - [periods.YEAR, periods.Instant((2022, 12, 1)), 1, 365], + [periods.DAY, periods.instant((2022, 12, 31)), 1, 1], + [periods.DAY, periods.instant((2022, 12, 31)), 3, 3], + [periods.MONTH, periods.instant((2012, 1, 3)), 3, 31 + 29 + 31], + [periods.MONTH, periods.instant((2012, 2, 3)), 1, 29], + [periods.MONTH, periods.instant((2022, 1, 3)), 3, 31 + 28 + 31], + [periods.MONTH, periods.instant((2022, 12, 1)), 1, 31], + [periods.YEAR, periods.instant((2012, 1, 1)), 1, 366], + [periods.YEAR, periods.instant((2022, 1, 1)), 2, 730], + [periods.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)) + period = periods.period((date_unit, instant, size)) assert period.count(periods.DAY) == expected @pytest.mark.parametrize("arg, expected", [ - ["1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], - ["1004-02-29", periods.Period((periods.DAY, periods.Instant((1004, 2, 29)), 1))], - ["ETERNITY", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], - ["day:1000-01-01", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 1))], - ["day:1000-01-01:3", periods.Period((periods.DAY, periods.Instant((1000, 1, 1)), 3))], - ["eternity", periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], - ["month:1000-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["month:1000-01-01", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 1))], - ["month:1000-01-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], - ["month:1000-01:3", periods.Period((periods.MONTH, periods.Instant((1000, 1, 1)), 3))], - ["year:1000", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["year:1000-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["year:1000-01-01", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - ["year:1000-01-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], - ["year:1000-01:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], - ["year:1000:3", periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 3))], - [1000, periods.Period((periods.YEAR, periods.Instant((1000, 1, 1)), 1))], - [periods.ETERNITY, periods.Period((periods.ETERNITY, periods.Instant((1, 1, 1)), 1))], - [periods.Instant((1, 1, 1)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 1))], - [periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365)), periods.Period((periods.DAY, periods.Instant((1, 1, 1)), 365))], + ["1000", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], + ["1000-01", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 1))], + ["1000-01-01", periods.period((periods.DAY, periods.instant((1000, 1, 1)), 1))], + ["1004-02-29", periods.period((periods.DAY, periods.instant((1004, 2, 29)), 1))], + ["ETERNITY", periods.period((periods.ETERNITY, periods.instant((1, 1, 1)), 1))], + ["day:1000-01-01", periods.period((periods.DAY, periods.instant((1000, 1, 1)), 1))], + ["day:1000-01-01:3", periods.period((periods.DAY, periods.instant((1000, 1, 1)), 3))], + ["eternity", periods.period((periods.ETERNITY, periods.instant((1, 1, 1)), 1))], + ["month:1000-01", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 1))], + ["month:1000-01-01", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 1))], + ["month:1000-01-01:3", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 3))], + ["month:1000-01:3", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 3))], + ["year:1000", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], + ["year:1000-01", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], + ["year:1000-01-01", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], + ["year:1000-01-01:3", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 3))], + ["year:1000-01:3", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 3))], + ["year:1000:3", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 3))], + [1000, periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], + [periods.ETERNITY, periods.period((periods.ETERNITY, periods.instant((1, 1, 1)), 1))], + [periods.instant((1, 1, 1)), periods.period((periods.DAY, periods.instant((1, 1, 1)), 1))], + [periods.period((periods.DAY, periods.instant((1, 1, 1)), 365)), periods.period((periods.DAY, periods.instant((1, 1, 1)), 365))], ]) def test_build(arg, expected): """Returns the expected ``Period``.""" - assert periods.Period.build(arg) == expected + assert periods.build(arg) == expected @pytest.mark.parametrize("arg, error", [ @@ -188,4 +188,4 @@ def test_build_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" with pytest.raises(error): - periods.Period.build(arg) + periods.build(arg) diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index 37fd10f301..fb47ae1071 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -112,7 +112,7 @@ def __call__( calculate: Calculate = Calculate( variable = variable_name, - period = periods.Period.build(period), + period = periods.build(period), option = options, ) diff --git a/openfisca_core/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index a9f71f3b3c..a9e1528290 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -186,7 +186,7 @@ def formula(self, simulation, period): @timeit def check_revenu_disponible(year, city_code, expected_revenu_disponible): - simulation = simulations.Simulation(period = periods.Period.build(year), tax_benefit_system = tax_benefit_system) + simulation = simulations.Simulation(period = periods.build(year), tax_benefit_system = tax_benefit_system) famille = simulation.populations["famille"] famille.count = 3 famille.roles_count = 2 diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 854a54f696..bb25ea9ec9 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,5 +1,7 @@ from __future__ import annotations +from openfisca_core.periods.typing import Period +from openfisca_core.types import Population, TaxBenefitSystem, Variable from typing import Dict, NamedTuple, Optional, Set import tempfile @@ -8,11 +10,17 @@ 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.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.build(period) + period = periods.build(period) self.tracer.record_calculation_start(variable_name, period) @@ -168,7 +176,7 @@ 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.build(period) + period = periods.build(period) # Check that the requested period matches definition_period if variable.definition_period > period.unit: @@ -197,7 +205,7 @@ 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.build(period) + period = periods.build(period) # Check that the requested period matches definition_period if variable.definition_period != periods.YEAR: @@ -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.build(period) + period = periods.build(period) return self.get_holder(variable_name).get_array(period) def get_holder(self, variable_name: str): @@ -438,7 +446,7 @@ def set_input(self, variable_name: str, period, value): if variable is None: raise VariableNotFoundError(variable_name, self.tax_benefit_system) - period = periods.Period.build(period) + period = periods.build(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 f6f0828205..99e0ee7673 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -325,7 +325,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.build(period_str)) + self.default_period = str(periods.build(period_str)) def get_input(self, variable, period_str): if variable not in self.input_buffer: @@ -368,7 +368,7 @@ def init_variable_values(self, entity, instance_object, instance_id): for period_str, value in variable_values.items(): try: - periods.Period.build(period_str) + periods.build(period_str) except ValueError as e: raise SituationParsingError(path_in_json, e.args[0]) variable = entity.get_variable(variable_name) @@ -393,7 +393,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.build(period_str))] = array + self.input_buffer[variable.name][str(periods.build(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 +411,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.build(period_str) for period_str in self.input_buffer[variable_name].keys()] + unsorted_periods = [periods.build(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 = lambda period: f"{period.unit}_{period.size}") diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 253e97a7c9..e4dc6e0c57 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -410,7 +410,7 @@ def get_parameters_at_instant( key = instant.start elif isinstance(instant, (str, int)): - key = periods.Instant.build(instant) + key = periods.instant.build(instant) else: msg = f"Expected an Instant (e.g. Instant((2017, 1, 1)) ). Got: {instant}." diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 44bb2ddb1d..7459ed482e 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, Period +from openfisca_core.types import Formula from typing import Optional, Union import datetime @@ -13,9 +15,6 @@ from openfisca_core import periods, tools from openfisca_core.entities import Entity from openfisca_core.indexed_enums import Enum, EnumArray -from openfisca_core.periods import Period -from openfisca_core.periods.typing import Instant -from openfisca_core.types import Formula from . import config, helpers @@ -340,10 +339,10 @@ def get_formula( else: try: - instant = periods.Period.build(period).start + instant = periods.build(period).start except ValueError: - instant = periods.Instant.build(period) + instant = periods.instant.build(period) if instant is None: return None 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..f1035d72ec 100644 --- a/tests/core/tax_scales/test_marginal_amount_tax_scale.py +++ b/tests/core/tax_scales/test_marginal_amount_tax_scale.py @@ -36,7 +36,7 @@ def test_calc(): # TODO: move, as we're testing Scale, not MarginalAmountTaxScale def test_dispatch_scale_type_on_creation(data): scale = parameters.Scale("amount_scale", data, "") - first_jan = periods.Instant((2017, 11, 1)) + first_jan = periods.instant((2017, 11, 1)) result = scale.get_at_instant(first_jan) 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..fdb3522523 100644 --- a/tests/core/tax_scales/test_single_amount_tax_scale.py +++ b/tests/core/tax_scales/test_single_amount_tax_scale.py @@ -50,7 +50,7 @@ def test_to_dict(): # TODO: move, as we're testing Scale, not SingleAmountTaxScale def test_assign_thresholds_on_creation(data): scale = parameters.Scale("amount_scale", data, "") - first_jan = periods.Instant((2017, 11, 1)) + first_jan = periods.instant((2017, 11, 1)) scale_at_instant = scale.get_at_instant(first_jan) result = scale_at_instant.thresholds @@ -61,7 +61,7 @@ def test_assign_thresholds_on_creation(data): # TODO: move, as we're testing Scale, not SingleAmountTaxScale def test_assign_amounts_on_creation(data): scale = parameters.Scale("amount_scale", data, "") - first_jan = periods.Instant((2017, 11, 1)) + first_jan = periods.instant((2017, 11, 1)) scale_at_instant = scale.get_at_instant(first_jan) result = scale_at_instant.amounts @@ -72,7 +72,7 @@ def test_assign_amounts_on_creation(data): # TODO: move, as we're testing Scale, not SingleAmountTaxScale def test_dispatch_scale_type_on_creation(data): scale = parameters.Scale("amount_scale", data, "") - first_jan = periods.Instant((2017, 11, 1)) + first_jan = periods.instant((2017, 11, 1)) result = scale.get_at_instant(first_jan) diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index f0aa2a4601..1a91b56b9a 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -5,7 +5,7 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable -PERIOD = periods.Period.build("2016-01") +PERIOD = periods.build("2016-01") @pytest.mark.parametrize("simulation", [({"salary": 2000}, PERIOD)], indirect = True) diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index 8ef363a3b1..7ee4c64a15 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.build('2013-01') + return periods.build('2013-01') @pytest.fixture diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index 22687e65fb..5f7bc827de 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -26,7 +26,7 @@ def couple(tax_benefit_system): build_from_entities(tax_benefit_system, situation_examples.couple) -period = periods.Period.build('2017-12') +period = periods.build('2017-12') def test_set_input_enum_string(couple): @@ -89,7 +89,7 @@ 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.build(periods.ETERNITY), value) + holder.set_input(periods.build(periods.ETERNITY), value) assert holder.get_array(None) == value assert holder.get_array(periods.ETERNITY) == value assert holder.get_array('2016-01') == value @@ -98,8 +98,8 @@ def test_permanent_variable_filled(single): def test_delete_arrays(single): simulation = single salary_holder = simulation.person.get_holder('salary') - salary_holder.set_input(periods.Period.build(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.Period.build(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.build(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 +109,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.build(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.build(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -119,7 +119,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.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.build(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 +132,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.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.build(2017), numpy.asarray([30000])) simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-02') @@ -147,7 +147,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.build(2019), 'owner') + holders.set_input_dispatch_by_period(holder, periods.build(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 +159,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.build(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.Period.build(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.build(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.build(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.build(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -172,7 +172,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.build('2017-01') + month = periods.build('2017-01') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -183,8 +183,8 @@ def test_cache_disk(couple): def test_known_periods(couple): simulation = couple simulation.memory_config = force_storage_on_disk - month = periods.Period.build('2017-01') - month_2 = periods.Period.build('2017-02') + month = periods.build('2017-01') + month_2 = periods.build('2017-02') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -196,7 +196,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.build('2017-01') + month = periods.build('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 92c97d4776..7a9ad4fa85 100644 --- a/tests/core/test_opt_out_cache.py +++ b/tests/core/test_opt_out_cache.py @@ -7,7 +7,7 @@ from openfisca_core.variables import Variable -PERIOD = periods.Period.build("2016-01") +PERIOD = periods.build("2016-01") class input(Variable): diff --git a/tests/core/test_projectors.py b/tests/core/test_projectors.py index 1fa3a759b3..3b32e5d9e2 100644 --- a/tests/core/test_projectors.py +++ b/tests/core/test_projectors.py @@ -1,8 +1,10 @@ +import numpy + +from openfisca_core import periods +from openfisca_core.entities import build_entity +from openfisca_core.model_api import Enum, Variable 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 def test_shortcut_to_containing_entity_provided(): @@ -125,14 +127,14 @@ class household_enum_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = household - definition_period = ETERNITY + definition_period = periods.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.ETERNITY def formula(person, period): return person.household("household_enum_variable", period) @@ -194,7 +196,7 @@ class household_projected_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = household - definition_period = ETERNITY + definition_period = periods.ETERNITY def formula(household, period): return household.value_from_first_person(household.members("person_enum_variable", period)) @@ -204,7 +206,7 @@ class person_enum_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = person - definition_period = ETERNITY + definition_period = periods.ETERNITY system.add_variables(household_projected_variable, person_enum_variable) @@ -275,14 +277,14 @@ class household_level_variable(Variable): possible_values = enum default_value = enum.FIRST_OPTION entity = household_entity - definition_period = ETERNITY + definition_period = periods.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.ETERNITY def formula(family, period): return family.household("household_level_variable", period) @@ -290,7 +292,7 @@ def formula(family, period): class decoded_projected_family_level_variable(Variable): value_type = str entity = family_entity - definition_period = ETERNITY + definition_period = periods.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 197ff50eaa..877a212b95 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -2,12 +2,12 @@ import pytest -from openfisca_core import periods -from openfisca_core.periods import Instant -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 import periods from openfisca_core.model_api import * # noqa analysis:ignore +from openfisca_core.parameters import ParameterNode, ValuesHistory +from openfisca_core.tools import assert_near class goes_to_school(Variable): @@ -15,7 +15,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.MONTH class WithBasicIncomeNeutralized(Reform): @@ -124,23 +124,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.build(2013).start, - periods.Period.build(2013).stop, + periods.build(2013).start, + periods.build(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.build(2015).start, - periods.Period.build(2015).stop, + periods.build(2015).start, + periods.build(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.build(2013).start, + periods.build(2013).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2013-01-01": {'value': 1.0}}), @@ -148,15 +148,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.build(2011).start, - periods.Period.build(2011).stop, + periods.build(2011).start, + periods.build(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.build(2015).start, + periods.build(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 +164,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.build(2014).start, - periods.Period.build(2014).stop, + periods.build(2014).start, + periods.build(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.build(2014).start, + periods.build(2014).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 1.0}}), @@ -180,23 +180,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.build(2005).start, - periods.Period.build(2005).stop, + periods.build(2005).start, + periods.build(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.build(2003).start, - periods.Period.build(2003).stop, + periods.build(2003).start, + periods.build(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.build(2005).start, + periods.build(2005).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2005-01-01": {'value': 1.0}}), @@ -204,7 +204,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.build(2006).start, + periods.build(2006).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 1.0}}), @@ -216,7 +216,7 @@ class new_variable(Variable): value_type = int label = "Nouvelle variable introduite par la réforme" entity = Household - definition_period = MONTH + definition_period = periods.MONTH def formula(household, period): return household.empty_array() + 10 @@ -240,7 +240,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.MONTH def formula_2010_01_01(household, period): return household.empty_array() + 10 @@ -263,7 +263,7 @@ def apply(self): def test_update_variable(make_simulation, tax_benefit_system): class disposable_income(Variable): - definition_period = MONTH + definition_period = periods.MONTH def formula_2018(household, period): return household.empty_array() + 10 @@ -294,7 +294,7 @@ def apply(self): def test_replace_variable(tax_benefit_system): class disposable_income(Variable): - definition_period = MONTH + definition_period = periods.MONTH entity = Person label = "Disposable income" value_type = float @@ -344,7 +344,7 @@ def apply(self): parameters_new_node = reform.parameters.children['new_node'] assert parameters_new_node is not None - instant = Instant((2013, 1, 1)) + instant = periods.instant((2013, 1, 1)) parameters_at_instant = reform.get_parameters_at_instant(instant) assert parameters_at_instant.new_node.new_param is True @@ -355,7 +355,7 @@ class some_variable(Variable): value_type = int entity = Person label = "Variable with many attributes" - definition_period = MONTH + definition_period = periods.MONTH set_input = set_input_divide_by_period calculate_output = calculate_output_add diff --git a/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index 38b242bd02..64ba052f1a 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -15,7 +15,7 @@ def monthly_variable(): class monthly_variable(Variable): value_type = int entity = Person - definition_period = MONTH + definition_period = periods.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.build(2019) + period = periods.build(2019) person = PopulationMock(monthly_variable) yearly_sum = sum( person('monthly_variable', month) - for month in period.subperiods(MONTH) + for month in period.subperiods(periods.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.build(2019) + period = periods.build(2019) annualized_variable = get_annualized_variable(monthly_variable) person = PopulationMock(annualized_variable) yearly_sum = sum( person('monthly_variable', month) - for month in period.subperiods(MONTH) + for month in period.subperiods(periods.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.build('year:2018:2') - annualized_variable = get_annualized_variable(monthly_variable, periods.Period.build(2018)) + period = periods.build('year:2018:2') + annualized_variable = get_annualized_variable(monthly_variable, periods.build(2018)) person = PopulationMock(annualized_variable) yearly_sum = sum( person('monthly_variable', month) - for month in period.subperiods(MONTH) + for month in period.subperiods(periods.MONTH) ) assert monthly_variable.calculation_count == 11 From 0f365de3e9bf9b5032b9cc953b083821704dd0d1 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 13:36:30 +0100 Subject: [PATCH 62/93] Fix documentation --- openfisca_core/periods/_units.py | 2 ++ openfisca_core/periods/period_.py | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_units.py index 8dee6e4545..e07d9dbc00 100644 --- a/openfisca_core/periods/_units.py +++ b/openfisca_core/periods/_units.py @@ -4,6 +4,8 @@ class DateUnitMeta(enum.EnumMeta): + """Metaclass for ``DateUnit``.""" + @property def isoformat(self) -> DateUnit: """Date units corresponding to the ISO format (day, month, and year). diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index e8e3d6e275..a86e51ab9a 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -230,9 +230,6 @@ def stop(self) -> Instant: Returns: An Instant. - Raises: - DateUnitValueError: If the period's unit isn't day, month or year. - Examples: >>> start = instant((2012, 2, 29)) @@ -282,7 +279,7 @@ def date(self) -> datetime.date: Traceback (most recent call last): ValueError: 'date' undefined for period size > 1: year:2021-10:3. - .. vesionchanged:: 39.0.0: + .. versionchanged:: 39.0.0: Made it a normal method instead of a property. """ @@ -545,7 +542,8 @@ def build(cls, value: Any) -> Period: :obj:`.Period`: A period. Raises: - PeriodFormatError: When the arguments were invalid, like "2021-32-13". + PeriodFormatError: When arguments are invalid, like "2021-32-13". + PeriodTypeError: When ``value`` is not a ``period-like`` object. Examples: >>> Period.build(Period((YEAR, instant((2021, 1, 1)), 1))) From b99c899363de66940c1d4815e0ff1b6680b085b4 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 13:53:08 +0100 Subject: [PATCH 63/93] Fix DateUnit types --- openfisca_core/periods/_units.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_units.py index e07d9dbc00..ceb082e63d 100644 --- a/openfisca_core/periods/_units.py +++ b/openfisca_core/periods/_units.py @@ -7,7 +7,7 @@ class DateUnitMeta(enum.EnumMeta): """Metaclass for ``DateUnit``.""" @property - def isoformat(self) -> DateUnit: + def isoformat(self) -> int: """Date units corresponding to the ISO format (day, month, and year). Returns: @@ -104,4 +104,4 @@ def __str__(self) -> str: return super().__str__() -DAY, MONTH, YEAR, ETERNITY = DateUnit +DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) From 2e05c76ad3ffcf9c27034389075d00ba69489c98 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 15:03:44 +0100 Subject: [PATCH 64/93] Fix period types --- openfisca_core/periods/instant_.py | 26 +++++++++---- openfisca_core/periods/period_.py | 61 +++++++++++++++++------------- openfisca_core/periods/typing.py | 15 ++++---- 3 files changed, 60 insertions(+), 42 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index d6e1fcfafa..0201148a8b 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -8,6 +8,7 @@ import inflect import pendulum +from pendulum.datetime import Date from ._config import INSTANT_PATTERN from ._errors import ( @@ -137,7 +138,7 @@ def day(self) -> int: return self[2] @functools.lru_cache(maxsize = None) - def date(self) -> datetime.date: + def date(self) -> Date: """The date representation of the ``Instant``. Example: @@ -152,12 +153,12 @@ def date(self) -> datetime.date: return pendulum.date(*self) - def offset(self, offset: str | int, unit: str) -> Instant: + def offset(self, offset: str | int, unit: int) -> Instant: """Increments/decrements the given instant with offset units. Args: offset (str | int): How much of ``unit`` to offset. - unit (str): What to offset. + unit (int): What to offset. Returns: Instant: A new one. @@ -183,7 +184,7 @@ def offset(self, offset: str | int, unit: str) -> Instant: year, month, day = self - if unit not in DateUnit.isoformat: + if not unit & DateUnit.isoformat: raise DateUnitValueError(unit) if offset in {"first-of", "last-of"} and unit == DAY: @@ -205,7 +206,7 @@ def offset(self, offset: str | int, unit: str) -> Instant: if not isinstance(offset, int): raise OffsetTypeError(offset) - date = self.date().add(**{self.plural(unit): offset}) + date = self.date().add(**{type(self).plural(str(unit)): offset}) return type(self)((date.year, date.month, date.day)) @@ -248,8 +249,11 @@ def build(cls, value: Any) -> Instant: Instant((2021, 9, 16)) .. versionadded:: 39.0.0 + """ + instant: Tuple[int, ...] | ISOFormat | None + if value is None or isinstance(value, DateUnit): raise InstantTypeError(value) @@ -266,13 +270,13 @@ def build(cls, value: Any) -> Instant: raise InstantValueError(value) if isinstance(value, str): - instant = ISOFormat.parse(value)[1:-1] + instant = ISOFormat.parse(value) elif isinstance(value, datetime.date): instant = value.year, value.month, value.day elif isinstance(value, int): - instant = value, + instant = value, 1, 1 elif isinstance(value, (dict, set)): raise InstantValueError(value) @@ -281,7 +285,10 @@ def build(cls, value: Any) -> Instant: raise InstantValueError(value) else: - instant = tuple(value) + instant = tuple(int(value) for value in tuple(value)) + + if instant is None: + raise InstantFormatError(value) if len(instant) == 1: return cls((instant[0], 1, 1)) @@ -289,4 +296,7 @@ def build(cls, value: Any) -> Instant: if len(instant) == 2: return cls((instant[0], instant[1], 1)) + if len(instant) == 5: + return cls(instant[1:-1]) + return cls(instant) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index a86e51ab9a..af733ec4dc 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -173,7 +173,7 @@ def __contains__(self, other: object) -> bool: return super().__contains__(other) @property - def unit(self) -> str: + def unit(self) -> int: """The ``unit`` of the ``Period``. Returns: @@ -252,7 +252,7 @@ def stop(self) -> Instant: stop = ( start .date() - .add(**{self.plural(unit): size}) + .add(**{type(self).plural(str(unit)): size}) .subtract(days = 1) ) @@ -289,7 +289,7 @@ def date(self) -> datetime.date: return self.start.date() - def count(self, unit: str) -> int: + def count(self, unit: int) -> int: """The ``size`` of the ``Period`` in the given unit. Args: @@ -344,15 +344,15 @@ def count(self, unit: str) -> int: return self.size * 12 raise ValueError( - f"Cannot calculate number of {self.plural(unit)} in a " + f"Cannot calculate number of {type(self).plural(str(unit))} in a " f"{str(self.unit)}." ) - def this(self, unit: str) -> Period: + def this(self, unit: int) -> Period: """A new month ``Period`` starting at the first of ``unit``. Args: - unit: The unit of the requested Period. + unit (int): The unit of the requested Period. Returns: A Period. @@ -377,12 +377,12 @@ def this(self, unit: str) -> Period: return type(self)((unit, self.start.offset("first-of", unit), 1)) - def last(self, unit: str, size: int = 1) -> Period: + def last(self, unit: int, size: int = 1) -> Period: """Last ``size`` ``unit``s of the ``Period``. Args: - unit: The unit of the requested Period. - size: The number of units to include in the Period. + unit (int): The unit of the requested Period. + size (int): The number of units to include in the Period. Returns: A Period. @@ -416,12 +416,12 @@ def last(self, unit: str, size: int = 1) -> Period: return type(self)((unit, self.ago(unit, size).start, size)) - def ago(self, unit: str, size: int = 1) -> Period: + def ago(self, unit: int, size: int = 1) -> Period: """``size`` ``unit``s ago of the ``Period``. Args: - unit: The unit of the requested Period. - size: The number of units ago. + unit (int): The unit of the requested Period. + size (int): The number of units ago. Returns: A Period. @@ -455,12 +455,12 @@ def ago(self, unit: str, size: int = 1) -> Period: return type(self)((unit, self.this(unit).start, 1)).offset(-size) - def offset(self, offset: str | int, unit: str | None = None) -> Period: + def offset(self, offset: str | int, unit: int | 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. + unit (int, optional): What to offset. Returns: Period: A new one. @@ -491,11 +491,11 @@ def offset(self, offset: str | int, unit: str | None = None) -> Period: return type(self)((self.unit, start, self.size)) - def subperiods(self, unit: str) -> Sequence[Period]: + def subperiods(self, unit: int) -> Sequence[Period]: """Return the list of all the periods of unit ``unit``. Args: - unit: A string representing period's ``unit``. + unit (int): A string representing period's ``unit``. Returns: A list of periods. @@ -523,7 +523,7 @@ def subperiods(self, unit: str) -> Sequence[Period]: if self.unit < unit: raise ValueError(f"Cannot subdivide {self.unit} into {unit}") - if unit not in DateUnit.isoformat: + if not unit & DateUnit.isoformat: raise DateUnitValueError(unit) return [ @@ -536,10 +536,10 @@ def build(cls, value: Any) -> Period: """Build a new period, aka a triple (unit, start_instant, size). Args: - value: A ``period-like`` object. + value (Any): A ``period-like`` object. Returns: - :obj:`.Period`: A period. + A period. Raises: PeriodFormatError: When arguments are invalid, like "2021-32-13". @@ -578,6 +578,10 @@ def build(cls, value: Any) -> Period: """ + unit: int | str + part: ISOFormat | None + size: int | str + if value in {ETERNITY, ETERNITY.name, ETERNITY.name.lower()}: return cls((ETERNITY, instant.build(datetime.date.min), 1)) @@ -597,10 +601,10 @@ def build(cls, value: Any) -> Period: raise PeriodFormatError(value) # Try to parse as a simple period - period = ISOFormat.parse(value) + part = ISOFormat.parse(value) - if period is not None: - return cls((DateUnit(period.unit), instant((period[1:-1])), 1)) + if part is not None: + return cls((DateUnit(part.unit), instant((part[1:-1])), 1)) # Complex periods must have a ':' in their strings if ":" not in value: @@ -619,12 +623,15 @@ def build(cls, value: Any) -> Period: unit = DateUnit[unit] # We get the first remaining component - period, *rest = rest + date, *rest = rest + + if date is None: + raise PeriodFormatError(value) # Middle component must be a valid ISO period - period = ISOFormat.parse(period) + part = ISOFormat.parse(date) - if period is None: + if part is None: raise PeriodFormatError(value) # Finally we try to parse the size, if any @@ -646,7 +653,7 @@ def build(cls, value: Any) -> Period: raise PeriodFormatError(value) # Reject ambiguous periods such as month:2014 - if period.unit > unit: + if part.unit > unit: raise PeriodFormatError(value) - return cls((unit, instant((period[1:-1])), size)) + return cls((unit, instant((part[1:-1])), size)) diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index 67eb615277..467754c032 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -7,7 +7,8 @@ from typing_extensions import Protocol import abc -import datetime + +from pendulum.datetime import Date @typing_extensions.runtime_checkable @@ -24,10 +25,10 @@ def __ge__(self, other: object) -> bool: ... def __le__(self, other: object) -> bool: ... @abc.abstractmethod - def date(self) -> datetime.date: ... + def date(self) -> Date: ... @abc.abstractmethod - def offset(self, offset: str | int, unit: str) -> Instant: ... + def offset(self, offset: str | int, unit: int) -> Instant: ... @typing_extensions.runtime_checkable @@ -37,15 +38,15 @@ def __iter__(self) -> Any: ... @property @abc.abstractmethod - def unit(self) -> Any: ... + def unit(self) -> int: ... @property @abc.abstractmethod - def start(self) -> Any: ... + def start(self) -> Instant: ... @property @abc.abstractmethod - def stop(self) -> Any: ... + def stop(self) -> Instant: ... @abc.abstractmethod - def offset(self, offset: Any, unit: Any = None) -> Any: ... + def offset(self, offset: str | int, unit: int | None = None) -> Period: ... From b45233d57b3e2a88d5afffb7a06465987839baa5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 15:13:51 +0100 Subject: [PATCH 65/93] Fix tracer types --- openfisca_core/tracers/full_tracer.py | 13 +++++-------- openfisca_core/tracers/simple_tracer.py | 10 +++------- openfisca_core/tracers/trace_node.py | 26 +++++++++++-------------- 3 files changed, 19 insertions(+), 30 deletions(-) diff --git a/openfisca_core/tracers/full_tracer.py b/openfisca_core/tracers/full_tracer.py index 6638a789d4..df9dc83865 100644 --- a/openfisca_core/tracers/full_tracer.py +++ b/openfisca_core/tracers/full_tracer.py @@ -1,17 +1,14 @@ from __future__ import annotations -import time -import typing +from numpy.typing import ArrayLike +from openfisca_core.periods.typing import Period from typing import Dict, Iterator, List, Optional, Union -from .. import tracers - -if typing.TYPE_CHECKING: - from numpy.typing import ArrayLike +import time - from openfisca_core.periods import Period +from .. import tracers - Stack = List[Dict[str, Union[str, Period]]] +Stack = List[Dict[str, Union[str, Period]]] class FullTracer: diff --git a/openfisca_core/tracers/simple_tracer.py b/openfisca_core/tracers/simple_tracer.py index 2fa98c6582..d03d49a7f4 100644 --- a/openfisca_core/tracers/simple_tracer.py +++ b/openfisca_core/tracers/simple_tracer.py @@ -1,14 +1,10 @@ from __future__ import annotations -import typing +from numpy.typing import ArrayLike +from openfisca_core.periods.typing import Period from typing import Dict, List, Union -if typing.TYPE_CHECKING: - from numpy.typing import ArrayLike - - from openfisca_core.periods import Period - - Stack = List[Dict[str, Union[str, Period]]] +Stack = List[Dict[str, Union[str, Period]]] class SimpleTracer: diff --git a/openfisca_core/tracers/trace_node.py b/openfisca_core/tracers/trace_node.py index 93b630886c..8b5af8e647 100644 --- a/openfisca_core/tracers/trace_node.py +++ b/openfisca_core/tracers/trace_node.py @@ -1,30 +1,26 @@ from __future__ import annotations -import dataclasses -import typing - -if typing.TYPE_CHECKING: - import numpy +from numpy.typing import ArrayLike +from openfisca_core.periods.typing import Period +from typing import List - from openfisca_core.indexed_enums import EnumArray - from openfisca_core.periods import Period +import dataclasses - Array = typing.Union[EnumArray, numpy.typing.ArrayLike] - Time = typing.Union[float, int] +from openfisca_core.indexed_enums import EnumArray @dataclasses.dataclass class TraceNode: name: str period: Period - parent: typing.Optional[TraceNode] = None - children: typing.List[TraceNode] = dataclasses.field(default_factory = list) - parameters: typing.List[TraceNode] = dataclasses.field(default_factory = list) - value: typing.Optional[Array] = None + parent: TraceNode | None = None + children: List[TraceNode] = dataclasses.field(default_factory = list) + parameters: List[TraceNode] = dataclasses.field(default_factory = list) + value: EnumArray | ArrayLike | None = None start: float = 0 end: float = 0 - def calculation_time(self, round_: bool = True) -> Time: + def calculation_time(self, round_: bool = True) -> float | int: result = self.end - self.start if round_: @@ -50,5 +46,5 @@ def append_child(self, node: TraceNode) -> None: self.children.append(node) @staticmethod - def round(time: Time) -> float: + def round(time: float | int) -> float: return float(f'{time:.4g}') # Keep only 4 significant figures From 24d0e84d3286b029789e9b89ae422f440fcd4159 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 15:31:16 +0100 Subject: [PATCH 66/93] Fix variable types --- openfisca_core/periods/period_.py | 45 +++++++++---------- openfisca_core/periods/typing.py | 14 +++--- .../taxbenefitsystems/tax_benefit_system.py | 3 -- openfisca_core/variables/variable.py | 10 ++--- 4 files changed, 33 insertions(+), 39 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index af733ec4dc..8895d72e06 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -9,8 +9,7 @@ from ._errors import DateUnitValueError, PeriodFormatError, PeriodTypeError from ._parsers import ISOFormat from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR -from .instant_ import Instant as instant -from .typing import Instant +from .instant_ import Instant class Period(Tuple[DateUnit, Instant, int]): @@ -31,7 +30,7 @@ class Period(Tuple[DateUnit, Instant, int]): The ``unit``, ``start``, and ``size``, accordingly. Examples: - >>> start = instant((2021, 9, 1)) + >>> start = Instant((2021, 9, 1)) >>> period = Period((YEAR, start, 3)) ``Periods`` are represented as a ``tuple`` containing the ``unit``, @@ -91,7 +90,7 @@ def __str__(self) -> str: str: A string representation of the period. Examples: - >>> jan = instant((2021, 1, 1)) + >>> jan = Instant((2021, 1, 1)) >>> feb = jan.offset(1, MONTH) >>> str(Period((YEAR, jan, 1))) @@ -159,7 +158,7 @@ def __contains__(self, other: object) -> bool: True if ``other`` is contained, otherwise False. Example: - >>> start = instant((2021, 1, 1)) + >>> start = Instant((2021, 1, 1)) >>> period = Period((YEAR, start, 1)) >>> sub_period = Period((MONTH, start, 3)) >>> sub_period in period @@ -180,7 +179,7 @@ def unit(self) -> int: An int. Example: - >>> start = instant((2021, 10, 1)) + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) >>> period.unit year @@ -197,7 +196,7 @@ def start(self) -> Instant: An Instant. Example: - >>> start = instant((2021, 10, 1)) + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) >>> period.start Instant((2021, 10, 1)) @@ -214,7 +213,7 @@ def size(self) -> int: An int. Example: - >>> start = instant((2021, 10, 1)) + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) >>> period.size 3 @@ -231,7 +230,7 @@ def stop(self) -> Instant: An Instant. Examples: - >>> start = instant((2012, 2, 29)) + >>> start = Instant((2012, 2, 29)) >>> Period((YEAR, start, 2)).stop Instant((2014, 2, 27)) @@ -268,7 +267,7 @@ def date(self) -> datetime.date: ValueError: If the period's size is greater than 1. Examples: - >>> start = instant((2021, 10, 1)) + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 1)) >>> period.date() @@ -302,7 +301,7 @@ def count(self, unit: int) -> int: ValueError: If the period's unit is not a day, a month or a year. Examples: - >>> start = instant((2021, 10, 1)) + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) >>> period.count(DAY) @@ -358,7 +357,7 @@ def this(self, unit: int) -> Period: A Period. Examples: - >>> start = instant((2023, 1, 1)) + >>> start = Instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -388,7 +387,7 @@ def last(self, unit: int, size: int = 1) -> Period: A Period. Examples: - >>> start = instant((2023, 1, 1)) + >>> start = Instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -427,7 +426,7 @@ def ago(self, unit: int, size: int = 1) -> Period: A Period. Examples: - >>> start = instant((2023, 1, 1)) + >>> start = Instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -466,7 +465,7 @@ def offset(self, offset: str | int, unit: int | None = None) -> Period: Period: A new one. Examples: - >>> start = instant((2014, 2, 3)) + >>> start = Instant((2014, 2, 3)) >>> Period((DAY, start, 1)).offset("first-of", MONTH) Period((day, Instant((2014, 2, 1)), 1)) @@ -474,7 +473,7 @@ def offset(self, offset: str | int, unit: int | None = None) -> Period: >>> Period((MONTH, start, 4)).offset("last-of", MONTH) Period((month, Instant((2014, 2, 28)), 4)) - >>> start = instant((2021, 1, 1)) + >>> start = Instant((2021, 1, 1)) >>> Period((DAY, start, 365)).offset(-3) Period((day, Instant((2020, 12, 29)), 365)) @@ -505,7 +504,7 @@ def subperiods(self, unit: int) -> Sequence[Period]: ValueError: If the period's unit is smaller than the given unit. Examples: - >>> start = instant((2021, 1, 1)) + >>> start = Instant((2021, 1, 1)) >>> period = Period((YEAR, start, 1)) >>> period.subperiods(MONTH) @@ -546,10 +545,10 @@ def build(cls, value: Any) -> Period: PeriodTypeError: When ``value`` is not a ``period-like`` object. Examples: - >>> Period.build(Period((YEAR, instant((2021, 1, 1)), 1))) + >>> Period.build(Period((YEAR, Instant((2021, 1, 1)), 1))) Period((year, Instant((2021, 1, 1)), 1)) - >>> Period.build(instant((2021, 1, 1))) + >>> Period.build(Instant((2021, 1, 1))) Period((day, Instant((2021, 1, 1)), 1)) >>> Period.build(ETERNITY) @@ -583,7 +582,7 @@ def build(cls, value: Any) -> Period: size: int | str if value in {ETERNITY, ETERNITY.name, ETERNITY.name.lower()}: - return cls((ETERNITY, instant.build(datetime.date.min), 1)) + return cls((ETERNITY, Instant.build(datetime.date.min), 1)) if value is None or isinstance(value, DateUnit): raise PeriodTypeError(value) @@ -595,7 +594,7 @@ def build(cls, value: Any) -> Period: return cls((DAY, value, 1)) if isinstance(value, int): - return cls((YEAR, instant((value, 1, 1)), 1)) + return cls((YEAR, Instant((value, 1, 1)), 1)) if not isinstance(value, str): raise PeriodFormatError(value) @@ -604,7 +603,7 @@ def build(cls, value: Any) -> Period: part = ISOFormat.parse(value) if part is not None: - return cls((DateUnit(part.unit), instant((part[1:-1])), 1)) + return cls((DateUnit(part.unit), Instant((part[1:-1])), 1)) # Complex periods must have a ':' in their strings if ":" not in value: @@ -656,4 +655,4 @@ def build(cls, value: Any) -> Period: if part.unit > unit: raise PeriodFormatError(value) - return cls((unit, instant((part[1:-1])), size)) + return cls((unit, Instant((part[1:-1])), size)) diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index 467754c032..e76296c4bf 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -19,10 +19,10 @@ def __init__(cls, *args: Iterable[int]) -> None: ... def __iter__(self) -> Iterator[int]: ... @abc.abstractmethod - def __ge__(self, other: object) -> bool: ... + def __ge__(self, other: Instant) -> bool: ... @abc.abstractmethod - def __le__(self, other: object) -> bool: ... + def __le__(self, other: Instant) -> bool: ... @abc.abstractmethod def date(self) -> Date: ... @@ -44,9 +44,9 @@ def unit(self) -> int: ... @abc.abstractmethod def start(self) -> Instant: ... - @property - @abc.abstractmethod - def stop(self) -> Instant: ... + # @property + # @abc.abstractmethod + # def stop(self) -> Instant: ... - @abc.abstractmethod - def offset(self, offset: str | int, unit: int | None = None) -> Period: ... + # @abc.abstractmethod + # def offset(self, offset: str | int, unit: int | None = None) -> Period: ... diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index e4dc6e0c57..d7112bf60a 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -400,9 +400,6 @@ def get_parameters_at_instant( """ - key: Optional[Instant] - msg: str - if isinstance(instant, Instant): key = instant diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 7459ed482e..feda98dcea 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -1,8 +1,6 @@ from __future__ import annotations -from openfisca_core.periods.typing import Instant, Period from openfisca_core.types import Formula -from typing import Optional, Union import datetime import inspect @@ -311,8 +309,8 @@ def get_introspection_data(cls, tax_benefit_system): def get_formula( self, - period: Union[Instant, Period, str, int] = None, - ) -> Optional[Formula]: + period: periods.period | periods.instant | str | int | None = None, + ) -> Formula | None: """Returns the formula to compute the variable at the given period. If no period is given and the variable has several formulas, the method @@ -326,7 +324,7 @@ def get_formula( """ - instant: Optional[Instant] + instant: periods.instant | None if not self.formulas: return None @@ -334,7 +332,7 @@ def get_formula( if period is None: return self.formulas.peekitem(index = 0)[1] # peekitem gets the 1st key-value tuple (the oldest start_date and formula). Return the formula. - if isinstance(period, Period): + if isinstance(period, periods.period): instant = period.start else: From 2922ea68453569848cbcdbfeffc249849660defc Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 16:46:23 +0100 Subject: [PATCH 67/93] Fix Instant.add types --- openfisca_core/holders/holder.py | 9 +- openfisca_core/periods/instant_.py | 27 ++++- openfisca_core/periods/typing.py | 103 +++++++++--------- openfisca_core/populations/population.py | 11 +- openfisca_core/simulations/simulation.py | 15 ++- .../taxbenefitsystems/tax_benefit_system.py | 13 +-- openfisca_core/tracers/full_tracer.py | 11 +- openfisca_core/tracers/simple_tracer.py | 7 +- openfisca_core/tracers/trace_node.py | 4 +- openfisca_core/variables/helpers.py | 3 +- 10 files changed, 109 insertions(+), 94 deletions(-) diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index c734625d85..19c3725df2 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Optional, Sequence, Union +from typing import Any, Sequence import os import warnings @@ -13,7 +13,6 @@ from openfisca_core import errors from openfisca_core import indexed_enums as enums from openfisca_core import periods, tools -from openfisca_core.periods.typing import Period from .memory_usage import MemoryUsage @@ -161,9 +160,9 @@ def get_known_periods(self): def set_input( self, - period: Period, - array: Union[numpy.ndarray, Sequence[Any]], - ) -> Optional[numpy.ndarray]: + period: periods.period, + array: numpy.ndarray | Sequence[Any], + ) -> numpy.ndarray | None: """Set a Variable's array of values of a given Period. Args: diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 0201148a8b..bad7744667 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -20,7 +20,6 @@ ) from ._parsers import ISOFormat from ._units import DateUnit, DAY, MONTH, YEAR -from .typing import Period class Instant(Tuple[int, int, int]): @@ -153,6 +152,26 @@ def date(self) -> Date: return pendulum.date(*self) + def add(self, unit: str, size: int) -> Date: + """Adds a given ``size`` amount to a date. + + Args: + unit (str): The unit to add. + size (int): The amount to add. + + Returns: + A date. + + Examples: + >>> Instant((2020, 1, 1)).add("years", 1) + Date(2021, 1, 1) + + """ + + add: Callable[..., Date] = self.date().add + + return add(**{unit: size}) + def offset(self, offset: str | int, unit: int) -> Instant: """Increments/decrements the given instant with offset units. @@ -206,7 +225,7 @@ def offset(self, offset: str | int, unit: int) -> Instant: if not isinstance(offset, int): raise OffsetTypeError(offset) - date = self.date().add(**{type(self).plural(str(unit)): offset}) + date = self.add(type(self).plural(str(unit)), offset) return type(self)((date.year, date.month, date.day)) @@ -215,7 +234,7 @@ def build(cls, value: Any) -> Instant: """Build a new instant, aka a triple of integers (year, month, day). Args: - value: An ``instant-like`` object. + value (Any): An ``instant-like`` object. Returns: An Instant. @@ -260,7 +279,7 @@ def build(cls, value: Any) -> Instant: if isinstance(value, Instant): return value - if isinstance(value, Period): + if hasattr(value, "start"): return value.start if isinstance(value, str) and not INSTANT_PATTERN.match(value): diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index e76296c4bf..ad9db6743f 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -1,52 +1,51 @@ -# pylint: disable=missing-class-docstring,missing-function-docstring - -from __future__ import annotations - -import typing_extensions -from typing import Any, Iterable, Iterator -from typing_extensions import Protocol - -import abc - -from pendulum.datetime import Date - - -@typing_extensions.runtime_checkable -class Instant(Protocol): - def __init__(cls, *args: Iterable[int]) -> None: ... - - @abc.abstractmethod - def __iter__(self) -> Iterator[int]: ... - - @abc.abstractmethod - def __ge__(self, other: Instant) -> bool: ... - - @abc.abstractmethod - def __le__(self, other: Instant) -> bool: ... - - @abc.abstractmethod - def date(self) -> Date: ... - - @abc.abstractmethod - def offset(self, offset: str | int, unit: int) -> Instant: ... - - -@typing_extensions.runtime_checkable -class Period(Protocol): - @abc.abstractmethod - def __iter__(self) -> Any: ... - - @property - @abc.abstractmethod - def unit(self) -> int: ... - - @property - @abc.abstractmethod - def start(self) -> Instant: ... - - # @property - # @abc.abstractmethod - # def stop(self) -> Instant: ... - - # @abc.abstractmethod - # def offset(self, offset: str | int, unit: int | None = None) -> Period: ... +# # pylint: disable=missing-class-docstring,missing-function-docstring +# +# from __future__ import annotations +# +# from typing import Any, Iterable, Iterator, TypeVar +# from typing_extensions import Protocol +# +# import abc +# +# from pendulum.datetime import Date +# +# T = TypeVar("T") +# +# +# class Instant(Protocol[~T]): +# def __init__(cls, *args: Iterable[int]) -> None: ... +# +# @abc.abstractmethod +# def __iter__(self) -> Iterator[int]: ... +# +# @abc.abstractmethod +# def __ge__(self, other: T) -> bool: ... +# +# @abc.abstractmethod +# def __le__(self, other: T) -> bool: ... +# +# @abc.abstractmethod +# def date(self) -> Date: ... +# +# @abc.abstractmethod +# def offset(self, offset: str | int, unit: int) -> T: ... +# +# +# class Period(Protocol): +# @abc.abstractmethod +# def __iter__(self) -> Any: ... +# +# @property +# @abc.abstractmethod +# def unit(self) -> int: ... +# +# @property +# @abc.abstractmethod +# def start(self) -> T: ... +# +# # @property +# # @abc.abstractmethod +# # def stop(self) -> Instant: ... +# +# # @abc.abstractmethod +# # def offset(self, offset: str | int, unit: int | None = None) -> Period: ... diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index fb47ae1071..b79c34760d 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -10,7 +10,6 @@ from openfisca_core import periods, projectors from openfisca_core.holders import Holder, MemoryUsage from openfisca_core.projectors import Projector -from openfisca_core.periods.typing import Period from openfisca_core.types import Array, Entity, Role, Simulation from . import config @@ -76,9 +75,9 @@ def check_array_compatible_with_entity( def check_period_validity( self, variable_name: str, - period: Optional[Union[int, str, Period]], + period: periods.period | int | str | None, ) -> None: - if isinstance(period, (int, str, Period)): + if isinstance(period, (int, str, periods.period)): return None stack = traceback.extract_stack() @@ -94,7 +93,7 @@ def check_period_validity( def __call__( self, variable_name: str, - period: Optional[Union[int, str, Period]] = None, + period: periods.period | int | str | None = None, options: Optional[Sequence[str]] = None, ) -> Optional[Array[float]]: """ @@ -266,8 +265,8 @@ def get_rank( class Calculate(NamedTuple): variable: str - period: Period - option: Optional[Sequence[str]] + period: periods.period + option: Sequence[str] | None class MemoryUsageByVariable(TypedDict, total = False): diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index bb25ea9ec9..41f11973e7 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -1,6 +1,5 @@ from __future__ import annotations -from openfisca_core.periods.typing import Period from openfisca_core.types import Population, TaxBenefitSystem, Variable from typing import Dict, NamedTuple, Optional, Set @@ -103,7 +102,7 @@ def data_storage_dir(self): def calculate(self, variable_name: str, period): """Calculate ``variable_name`` for ``period``.""" - if period is not None and not isinstance(period, Period): + if period is not None and not isinstance(period, periods.period): period = periods.build(period) self.tracer.record_calculation_start(variable_name, period) @@ -117,7 +116,7 @@ def calculate(self, variable_name: str, period): self.tracer.record_calculation_end() self.purge_cache_of_invalid_values() - def _calculate(self, variable_name: str, period: Period): + def _calculate(self, variable_name: str, period: periods.period): """ Calculate the variable ``variable_name`` for the period ``period``, using the variable formula if it exists. @@ -175,7 +174,7 @@ def calculate_add(self, variable_name: str, period): if variable is None: raise VariableNotFoundError(variable_name, self.tax_benefit_system) - if period is not None and not isinstance(period, Period): + if period is not None and not isinstance(period, periods.period): period = periods.build(period) # Check that the requested period matches definition_period @@ -204,7 +203,7 @@ def calculate_divide(self, variable_name: str, period): if variable is None: raise VariableNotFoundError(variable_name, self.tax_benefit_system) - if period is not None and not isinstance(period, Period): + if period is not None and not isinstance(period, periods.period): period = periods.build(period) # Check that the requested period matches definition_period @@ -352,7 +351,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): + if period is not None and not isinstance(period, periods.period): period = periods.build(period) return self.get_holder(variable_name).get_array(period) @@ -418,7 +417,7 @@ def get_known_periods(self, variable): >>> simulation.set_input('age', '2018-04', [12, 14]) >>> simulation.set_input('age', '2018-05', [13, 14]) >>> simulation.get_known_periods('age') - [Period((u'month', Instant((2018, 5, 1)), 1)), Period((u'month', Instant((2018, 4, 1)), 1))] + [periods.period((u'month', Instant((2018, 5, 1)), 1)), periods.period((u'month', Instant((2018, 4, 1)), 1))] """ return self.get_holder(variable).get_known_periods() @@ -502,4 +501,4 @@ def clone(self, debug = False, trace = False): class Cache(NamedTuple): variable: str - period: Period + period: periods.period diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index d7112bf60a..38349bd916 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -1,8 +1,7 @@ from __future__ import annotations import typing -from openfisca_core.periods.typing import Instant, Period -from typing import Any, Dict, Optional, Sequence, Union +from typing import Any, Dict, Optional, Sequence import copy import functools @@ -46,7 +45,7 @@ class TaxBenefitSystem: person_entity: Entity _base_tax_benefit_system = None - _parameters_at_instant_cache: Dict[Instant, types.ParameterNodeAtInstant] = {} + _parameters_at_instant_cache: Dict[periods.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. @@ -345,7 +344,7 @@ def neutralize_variable(self, variable_name: str): def annualize_variable( self, variable_name: str, - period: Optional[Period] = None, + period: periods.period | None = None, ) -> None: check: bool variable: Optional[Variable] @@ -388,7 +387,7 @@ def _get_baseline_parameters_at_instant(self, instant): @functools.lru_cache() def get_parameters_at_instant( self, - instant: Union[str, int, Period, Instant], + instant: periods.period | periods.instant | str | int, ) -> Optional[types.ParameterNodeAtInstant]: """Get the parameters of the legislation at a given instant @@ -400,10 +399,10 @@ def get_parameters_at_instant( """ - if isinstance(instant, Instant): + if isinstance(instant, periods.instant): key = instant - elif isinstance(instant, Period): + elif isinstance(instant, periods.period): key = instant.start elif isinstance(instant, (str, int)): diff --git a/openfisca_core/tracers/full_tracer.py b/openfisca_core/tracers/full_tracer.py index df9dc83865..6edb42d937 100644 --- a/openfisca_core/tracers/full_tracer.py +++ b/openfisca_core/tracers/full_tracer.py @@ -1,14 +1,15 @@ from __future__ import annotations from numpy.typing import ArrayLike -from openfisca_core.periods.typing import Period from typing import Dict, Iterator, List, Optional, Union import time +from openfisca_core import periods + from .. import tracers -Stack = List[Dict[str, Union[str, Period]]] +Stack = List[Dict[str, Union[str, periods.period]]] class FullTracer: @@ -25,7 +26,7 @@ def __init__(self) -> None: def record_calculation_start( self, variable: str, - period: Period, + period: periods.period, ) -> None: self._simple_tracer.record_calculation_start(variable, period) self._enter_calculation(variable, period) @@ -34,7 +35,7 @@ def record_calculation_start( def _enter_calculation( self, variable: str, - period: Period, + period: periods.period, ) -> None: new_node = tracers.TraceNode( name = variable, @@ -53,7 +54,7 @@ def _enter_calculation( def record_parameter_access( self, parameter: str, - period: Period, + period: periods.period, value: ArrayLike, ) -> None: diff --git a/openfisca_core/tracers/simple_tracer.py b/openfisca_core/tracers/simple_tracer.py index d03d49a7f4..ab0de1349d 100644 --- a/openfisca_core/tracers/simple_tracer.py +++ b/openfisca_core/tracers/simple_tracer.py @@ -1,10 +1,11 @@ from __future__ import annotations from numpy.typing import ArrayLike -from openfisca_core.periods.typing import Period from typing import Dict, List, Union -Stack = List[Dict[str, Union[str, Period]]] +from openfisca_core import periods + +Stack = List[Dict[str, Union[str, periods.period]]] class SimpleTracer: @@ -14,7 +15,7 @@ class SimpleTracer: def __init__(self) -> None: self._stack = [] - def record_calculation_start(self, variable: str, period: Period) -> None: + def record_calculation_start(self, variable: str, period: periods.period) -> None: self.stack.append({'name': variable, 'period': period}) def record_calculation_result(self, value: ArrayLike) -> None: diff --git a/openfisca_core/tracers/trace_node.py b/openfisca_core/tracers/trace_node.py index 8b5af8e647..753d3c33b7 100644 --- a/openfisca_core/tracers/trace_node.py +++ b/openfisca_core/tracers/trace_node.py @@ -1,18 +1,18 @@ from __future__ import annotations from numpy.typing import ArrayLike -from openfisca_core.periods.typing import Period from typing import List import dataclasses +from openfisca_core import periods from openfisca_core.indexed_enums import EnumArray @dataclasses.dataclass class TraceNode: name: str - period: Period + period: periods.period parent: TraceNode | None = None children: List[TraceNode] = dataclasses.field(default_factory = list) parameters: List[TraceNode] = dataclasses.field(default_factory = list) diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index 5131c538b3..283840db6b 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -1,6 +1,5 @@ from __future__ import annotations -from openfisca_core.periods.typing import Period from typing import Optional import sortedcontainers @@ -10,7 +9,7 @@ from .. import variables -def get_annualized_variable(variable: variables.Variable, annualization_period: Optional[Period] = None) -> variables.Variable: +def get_annualized_variable(variable: variables.Variable, annualization_period: Optional[periods.period] = None) -> variables.Variable: """ Returns a clone of ``variable`` that is annualized for the period ``annualization_period``. When annualized, a variable's formula is only called for a January calculation, and the results for other months are assumed to be identical. From d16552509a0d63299ef194f12a25fbe8f0cb75fc Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 17:17:48 +0100 Subject: [PATCH 68/93] Refactor into Add protocol --- openfisca_core/periods/instant_.py | 30 +++++++----------------------- openfisca_core/periods/period_.py | 15 +++++---------- openfisca_core/periods/typing.py | 23 +++++++++++++++++++---- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index bad7744667..f5de934d34 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Any, Callable, Tuple +from typing import Any, Tuple import calendar import datetime import functools +from .typing import Add, Plural + import inflect import pendulum from pendulum.datetime import Date @@ -76,7 +78,7 @@ class Instant(Tuple[int, int, int]): """ - plural: Callable[[str], str] = inflect.engine().plural + plural: Plural[str] = inflect.engine().plural def __repr__(self) -> str: return ( @@ -152,26 +154,6 @@ def date(self) -> Date: return pendulum.date(*self) - def add(self, unit: str, size: int) -> Date: - """Adds a given ``size`` amount to a date. - - Args: - unit (str): The unit to add. - size (int): The amount to add. - - Returns: - A date. - - Examples: - >>> Instant((2020, 1, 1)).add("years", 1) - Date(2021, 1, 1) - - """ - - add: Callable[..., Date] = self.date().add - - return add(**{unit: size}) - def offset(self, offset: str | int, unit: int) -> Instant: """Increments/decrements the given instant with offset units. @@ -225,7 +207,9 @@ def offset(self, offset: str | int, unit: int) -> Instant: if not isinstance(offset, int): raise OffsetTypeError(offset) - date = self.add(type(self).plural(str(unit)), offset) + add: Add[Date] = self.date().add + + date = add(**{type(self).plural(str(unit)): offset}) return type(self)((date.year, date.month, date.day)) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 8895d72e06..8a8f2dfa46 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Callable, Sequence, Tuple +from typing import Any, Sequence, Tuple import datetime @@ -11,6 +11,8 @@ from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR from .instant_ import Instant +from .typing import Plural + class Period(Tuple[DateUnit, Instant, int]): """Toolbox to handle date intervals. @@ -75,7 +77,7 @@ class Period(Tuple[DateUnit, Instant, int]): """ - plural: Callable[[str], str] = inflect.engine().plural + plural: Plural[str] = inflect.engine().plural def __repr__(self) -> str: return ( @@ -248,14 +250,7 @@ def stop(self) -> Instant: if unit == ETERNITY: return type(self.start)((1, 1, 1)) - stop = ( - start - .date() - .add(**{type(self).plural(str(unit)): size}) - .subtract(days = 1) - ) - - return type(start)((stop.year, stop.month, stop.day)) + return start.offset(size, unit).offset(-1, DAY) def date(self) -> datetime.date: """The date representation of the ``period``'s' start date. diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index ad9db6743f..35557cd35d 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -1,7 +1,22 @@ -# # pylint: disable=missing-class-docstring,missing-function-docstring -# -# from __future__ import annotations -# +# pylint: disable=missing-class-docstring,missing-function-docstring + +from __future__ import annotations + +from typing import Any, TypeVar +from typing_extensions import Protocol + +A = TypeVar("A", covariant = True) +P = TypeVar("P") + + +class Add(Protocol[A]): + def __call__(self, years: int, months: int, week: int, days: int) -> A: ... + + +class Plural(Protocol[P]): + def __call__(self, __arg1: P, __arg2: Any = None) -> P: ... + + # from typing import Any, Iterable, Iterator, TypeVar # from typing_extensions import Protocol # From 372c0e998f8c8a8161d4ed6d597e4ba68f3a3c87 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 18:35:32 +0100 Subject: [PATCH 69/93] Remove black magic from instant -> period --- openfisca_core/periods/instant_.py | 10 ++-- openfisca_core/periods/period_.py | 4 +- openfisca_core/periods/tests/test_instant.py | 2 +- openfisca_core/periods/typing.py | 63 +++----------------- openfisca_core/variables/variable.py | 2 - 5 files changed, 14 insertions(+), 67 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index f5de934d34..7536249c97 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -6,8 +6,6 @@ import datetime import functools -from .typing import Add, Plural - import inflect import pendulum from pendulum.datetime import Date @@ -22,6 +20,7 @@ ) from ._parsers import ISOFormat from ._units import DateUnit, DAY, MONTH, YEAR +from .typing import Add, Plural class Instant(Tuple[int, int, int]): @@ -248,8 +247,10 @@ def build(cls, value: Any) -> Instant: >>> start = Instant((2021, 9, 16)) >>> period = periods.period((YEAR, start, 1)) + >>> Instant.build(period) - Instant((2021, 9, 16)) + Traceback (most recent call last): + TypeError: int() argument must be a string, a bytes-like object ... .. versionadded:: 39.0.0 @@ -263,9 +264,6 @@ def build(cls, value: Any) -> Instant: if isinstance(value, Instant): return value - if hasattr(value, "start"): - return value.start - if isinstance(value, str) and not INSTANT_PATTERN.match(value): raise InstantFormatError(value) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 8a8f2dfa46..7e441f1995 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -10,7 +10,6 @@ from ._parsers import ISOFormat from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR from .instant_ import Instant - from .typing import Plural @@ -332,7 +331,8 @@ def count(self, unit: int) -> int: return self.size if unit == DAY and self.unit in {MONTH, YEAR}: - return (self.stop.date() - self.start.date()).days + 1 + delta: int = (self.stop.date() - self.start.date()).days + return delta + 1 if unit == MONTH and self.unit == YEAR: return self.size * 12 diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index dabecca5b4..6400c5fd8e 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -40,7 +40,6 @@ def test_offset(instant, offset, unit, expected): [(1000, 1, 1), periods.instant((1000, 1, 1))], [datetime.date(1, 1, 1), periods.instant((1, 1, 1))], [periods.instant((1, 1, 1)), periods.instant((1, 1, 1))], - [periods.period((periods.DAY, periods.instant((1, 1, 1)), 365)), periods.instant((1, 1, 1))], ]) def test_build_instant(arg, expected): """Returns the expected ``Instant``.""" @@ -66,6 +65,7 @@ def test_build_instant(arg, expected): [None, TypeError], [periods.ETERNITY, TypeError], [periods.YEAR, TypeError], + [periods.period((periods.DAY, periods.instant((1, 1, 1)), 365)), TypeError], ]) def test_build_instant_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index 35557cd35d..00e3ad0f46 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -2,65 +2,16 @@ from __future__ import annotations -from typing import Any, TypeVar +from typing import TypeVar from typing_extensions import Protocol -A = TypeVar("A", covariant = True) -P = TypeVar("P") +T = TypeVar("T", covariant = True) +U = TypeVar("U") -class Add(Protocol[A]): - def __call__(self, years: int, months: int, week: int, days: int) -> A: ... +class Add(Protocol[T]): + def __call__(self, years: int, months: int, week: int, days: int) -> T: ... -class Plural(Protocol[P]): - def __call__(self, __arg1: P, __arg2: Any = None) -> P: ... - - -# from typing import Any, Iterable, Iterator, TypeVar -# from typing_extensions import Protocol -# -# import abc -# -# from pendulum.datetime import Date -# -# T = TypeVar("T") -# -# -# class Instant(Protocol[~T]): -# def __init__(cls, *args: Iterable[int]) -> None: ... -# -# @abc.abstractmethod -# def __iter__(self) -> Iterator[int]: ... -# -# @abc.abstractmethod -# def __ge__(self, other: T) -> bool: ... -# -# @abc.abstractmethod -# def __le__(self, other: T) -> bool: ... -# -# @abc.abstractmethod -# def date(self) -> Date: ... -# -# @abc.abstractmethod -# def offset(self, offset: str | int, unit: int) -> T: ... -# -# -# class Period(Protocol): -# @abc.abstractmethod -# def __iter__(self) -> Any: ... -# -# @property -# @abc.abstractmethod -# def unit(self) -> int: ... -# -# @property -# @abc.abstractmethod -# def start(self) -> T: ... -# -# # @property -# # @abc.abstractmethod -# # def stop(self) -> Instant: ... -# -# # @abc.abstractmethod -# # def offset(self, offset: str | int, unit: int | None = None) -> Period: ... +class Plural(Protocol[U]): + def __call__(self, text: U, count: str | int | None = None) -> U: ... diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index feda98dcea..06b850472d 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -324,8 +324,6 @@ def get_formula( """ - instant: periods.instant | None - if not self.formulas: return None From 76f8863a622677a5dd0c0389e90247e155566fa2 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sat, 17 Dec 2022 19:38:06 +0100 Subject: [PATCH 70/93] Remove unused types --- openfisca_core/periods/instant_.py | 4 ++-- openfisca_core/periods/period_.py | 2 +- openfisca_core/periods/typing.py | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 7536249c97..2cdbfffa23 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -77,7 +77,7 @@ class Instant(Tuple[int, int, int]): """ - plural: Plural[str] = inflect.engine().plural + plural: Plural = inflect.engine().plural def __repr__(self) -> str: return ( @@ -206,7 +206,7 @@ def offset(self, offset: str | int, unit: int) -> Instant: if not isinstance(offset, int): raise OffsetTypeError(offset) - add: Add[Date] = self.date().add + add: Add = self.date().add date = add(**{type(self).plural(str(unit)): offset}) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 7e441f1995..ad949f6f77 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -76,7 +76,7 @@ class Period(Tuple[DateUnit, Instant, int]): """ - plural: Plural[str] = inflect.engine().plural + plural: Plural = inflect.engine().plural def __repr__(self) -> str: return ( diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index 00e3ad0f46..3274d2dd00 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -2,16 +2,16 @@ from __future__ import annotations -from typing import TypeVar from typing_extensions import Protocol -T = TypeVar("T", covariant = True) -U = TypeVar("U") +from pendulum.datetime import Date -class Add(Protocol[T]): - def __call__(self, years: int, months: int, week: int, days: int) -> T: ... +class Add(Protocol): + def __call__(self, years: int, months: int, week: int, days: int) -> Date: + ... -class Plural(Protocol[U]): - def __call__(self, text: U, count: str | int | None = None) -> U: ... +class Plural(Protocol): + def __call__(self, text: str, count: str | int | None = None) -> str: + ... From 129d701733f02c449fee693b783c81d3abccddec Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 18 Dec 2022 11:57:59 +0100 Subject: [PATCH 71/93] Improve readability of tests --- openfisca_core/periods/__init__.py | 25 +-- .../periods/{_units.py => _date_unit.py} | 3 - openfisca_core/periods/_errors.py | 8 +- openfisca_core/periods/instant_.py | 4 +- openfisca_core/periods/period_.py | 4 +- openfisca_core/periods/tests/test__parsers.py | 4 +- openfisca_core/periods/tests/test_instant.py | 54 ++--- openfisca_core/periods/tests/test_period.py | 188 +++++++++--------- 8 files changed, 149 insertions(+), 141 deletions(-) rename openfisca_core/periods/{_units.py => _date_unit.py} (97%) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 373d3afcb8..2d8a715466 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -27,16 +27,17 @@ """ -from . import _parsers as parsers -from . import _units as units +from ._parsers import ISOFormat +from ._date_unit import DateUnit from ._config import INSTANT_PATTERN -from .instant_ import Instant as instant -from .period_ import Period as period - -DAY = units.DAY -MONTH = units.MONTH -YEAR = units.YEAR -ETERNITY = units.ETERNITY -dateunit = units.DateUnit -build = period.build -isoformat = parsers.ISOFormat.parse +from .instant_ import Instant +from .period_ import Period + +DAY = DateUnit.DAY +MONTH = DateUnit.MONTH +YEAR = DateUnit.YEAR +ETERNITY = DateUnit.ETERNITY +dateunit = DateUnit +instant = Instant +period = Period +isoformat = ISOFormat diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_date_unit.py similarity index 97% rename from openfisca_core/periods/_units.py rename to openfisca_core/periods/_date_unit.py index ceb082e63d..d6b6885b5c 100644 --- a/openfisca_core/periods/_units.py +++ b/openfisca_core/periods/_date_unit.py @@ -102,6 +102,3 @@ def __str__(self) -> str: except AttributeError: return super().__str__() - - -DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index c629ee4772..81df8797f5 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -2,7 +2,9 @@ from typing import Any -from ._units import DAY, MONTH, YEAR +from ._date_unit import DateUnit + +day, month, year, _ = tuple(DateUnit) LEARN_MORE = ( "Learn more about legal period formats in OpenFisca: " @@ -16,8 +18,8 @@ class DateUnitValueError(ValueError): def __init__(self, value: Any) -> None: super().__init__( f"'{str(value)}' is not a valid ISO format date unit. ISO format " - f"date units are any of: '{str(DAY)}', '{str(MONTH)}', or " - f"'{str(YEAR)}'. {LEARN_MORE}" + f"date units are any of: '{str(day)}', '{str(month)}', or " + f"'{str(year)}'. {LEARN_MORE}" ) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 2cdbfffa23..dfe5abfc35 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -19,9 +19,11 @@ OffsetTypeError, ) from ._parsers import ISOFormat -from ._units import DateUnit, DAY, MONTH, YEAR +from ._date_unit import DateUnit from .typing import Add, Plural +DAY, MONTH, YEAR, _ = tuple(DateUnit) + class Instant(Tuple[int, int, int]): """An instant in time (``year``, ``month``, ``day``). diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index ad949f6f77..9bcbb7ff42 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -8,10 +8,12 @@ from ._errors import DateUnitValueError, PeriodFormatError, PeriodTypeError from ._parsers import ISOFormat -from ._units import DateUnit, DAY, ETERNITY, MONTH, YEAR +from ._date_unit import DateUnit from .instant_ import Instant from .typing import Plural +DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) + class Period(Tuple[DateUnit, Instant, int]): """Toolbox to handle date intervals. diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test__parsers.py index 428ac61a19..d16924bf47 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test__parsers.py @@ -1,6 +1,6 @@ import pytest -from openfisca_core import periods +from openfisca_core.periods import ISOFormat @pytest.mark.parametrize("arg, expected", [ @@ -17,4 +17,4 @@ def test_parse_iso_format(arg, expected): """Returns an ``ISOFormat`` when given a valid ISO format string.""" - assert periods.isoformat(arg) == expected + assert ISOFormat.parse(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 6400c5fd8e..b5010c2eb2 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -2,27 +2,29 @@ import pytest -from openfisca_core import periods +from openfisca_core.periods import Instant, Period, DateUnit + +day, month, year, eternity = DateUnit @pytest.fixture def instant(): """Returns a ``Instant``.""" - return periods.instant((2020, 2, 29)) + return Instant((2020, 2, 29)) @pytest.mark.parametrize("offset, unit, expected", [ - ["first-of", periods.MONTH, periods.instant((2020, 2, 1))], - ["first-of", periods.YEAR, periods.instant((2020, 1, 1))], - ["last-of", periods.MONTH, periods.instant((2020, 2, 29))], - ["last-of", periods.YEAR, periods.instant((2020, 12, 31))], - [-3, periods.DAY, periods.instant((2020, 2, 26))], - [-3, periods.MONTH, periods.instant((2019, 11, 29))], - [-3, periods.YEAR, periods.instant((2017, 2, 28))], - [3, periods.DAY, periods.instant((2020, 3, 3))], - [3, periods.MONTH, periods.instant((2020, 5, 29))], - [3, periods.YEAR, periods.instant((2023, 2, 28))], + ["first-of", month, Instant((2020, 2, 1))], + ["first-of", year, Instant((2020, 1, 1))], + ["last-of", month, Instant((2020, 2, 29))], + ["last-of", year, Instant((2020, 12, 31))], + [-3, day, Instant((2020, 2, 26))], + [-3, month, Instant((2019, 11, 29))], + [-3, year, Instant((2017, 2, 28))], + [3, day, Instant((2020, 3, 3))], + [3, month, Instant((2020, 5, 29))], + [3, year, Instant((2023, 2, 28))], ]) def test_offset(instant, offset, unit, expected): """Returns the expected ``Instant``.""" @@ -31,20 +33,20 @@ def test_offset(instant, offset, unit, expected): @pytest.mark.parametrize("arg, expected", [ - ["1000", periods.instant((1000, 1, 1))], - ["1000-01", periods.instant((1000, 1, 1))], - ["1000-01-01", periods.instant((1000, 1, 1))], - [1000, periods.instant((1000, 1, 1))], - [(1000,), periods.instant((1000, 1, 1))], - [(1000, 1), periods.instant((1000, 1, 1))], - [(1000, 1, 1), periods.instant((1000, 1, 1))], - [datetime.date(1, 1, 1), periods.instant((1, 1, 1))], - [periods.instant((1, 1, 1)), periods.instant((1, 1, 1))], + ["1000", Instant((1000, 1, 1))], + ["1000-01", Instant((1000, 1, 1))], + ["1000-01-01", Instant((1000, 1, 1))], + [1000, Instant((1000, 1, 1))], + [(1000,), Instant((1000, 1, 1))], + [(1000, 1), Instant((1000, 1, 1))], + [(1000, 1, 1), Instant((1000, 1, 1))], + [datetime.date(1, 1, 1), Instant((1, 1, 1))], + [Instant((1, 1, 1)), Instant((1, 1, 1))], ]) def test_build_instant(arg, expected): """Returns the expected ``Instant``.""" - assert periods.instant.build(arg) == expected + assert Instant.build(arg) == expected @pytest.mark.parametrize("arg, error", [ @@ -63,12 +65,12 @@ def test_build_instant(arg, expected): ["year:1000-01-01:1", ValueError], ["year:1000-01-01:3", ValueError], [None, TypeError], - [periods.ETERNITY, TypeError], - [periods.YEAR, TypeError], - [periods.period((periods.DAY, periods.instant((1, 1, 1)), 365)), TypeError], + [eternity, TypeError], + [year, TypeError], + [Period((day, Instant((1, 1, 1)), 365)), TypeError], ]) def test_build_instant_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" with pytest.raises(error): - periods.instant.build(arg) + Instant.build(arg) diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index be92e430a3..4339b173db 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -2,166 +2,168 @@ import pytest -from openfisca_core import periods +from openfisca_core.periods import DateUnit, Period, Instant + +day, month, year, eternity = DateUnit @pytest.fixture def instant(): """Returns a ``Instant``.""" - return periods.instant((2022, 12, 31)) + return Instant((2022, 12, 31)) @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, periods.instant((2022, 1, 1)), 12, "2022"], - [periods.MONTH, periods.instant((2022, 3, 1)), 12, "year:2022-03"], - [periods.YEAR, periods.instant((2022, 1, 1)), 1, "2022"], - [periods.YEAR, periods.instant((2022, 1, 1)), 3, "year:2022:3"], - [periods.YEAR, periods.instant((2022, 1, 3)), 3, "year:2022:3"], - [periods.YEAR, periods.instant((2022, 3, 1)), 1, "year:2022-03"], + [month, Instant((2022, 1, 1)), 12, "2022"], + [month, Instant((2022, 3, 1)), 12, "year:2022-03"], + [year, Instant((2022, 1, 1)), 1, "2022"], + [year, Instant((2022, 1, 1)), 3, "year:2022:3"], + [year, Instant((2022, 1, 3)), 3, "year:2022:3"], + [year, Instant((2022, 3, 1)), 1, "year:2022-03"], ]) def test_str_with_years(date_unit, instant, size, expected): """Returns the expected string.""" - assert str(periods.period((date_unit, instant, size))) == expected + assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, periods.instant((2022, 1, 1)), 1, "2022-01"], - [periods.MONTH, periods.instant((2022, 1, 1)), 3, "month:2022-01:3"], - [periods.MONTH, periods.instant((2022, 3, 1)), 3, "month:2022-03:3"], + [month, Instant((2022, 1, 1)), 1, "2022-01"], + [month, Instant((2022, 1, 1)), 3, "month:2022-01:3"], + [month, 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 + assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.DAY, periods.instant((2022, 1, 1)), 1, "2022-01-01"], - [periods.DAY, periods.instant((2022, 1, 1)), 3, "day:2022-01-01:3"], - [periods.DAY, periods.instant((2022, 3, 1)), 3, "day:2022-03-01:3"], + [day, Instant((2022, 1, 1)), 1, "2022-01-01"], + [day, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], + [day, 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 + assert str(Period((date_unit, instant, size))) == expected @pytest.mark.parametrize("period_unit, unit, start, cease, count", [ - [periods.DAY, periods.DAY, periods.instant((2022, 12, 31)), periods.instant((2023, 1, 2)), 3], - [periods.MONTH, periods.DAY, periods.instant((2022, 12, 31)), periods.instant((2023, 3, 30)), 90], - [periods.MONTH, periods.MONTH, periods.instant((2022, 12, 1)), periods.instant((2023, 2, 1)), 3], - [periods.YEAR, periods.DAY, periods.instant((2022, 12, 31)), periods.instant((2025, 12, 30)), 1096], - [periods.YEAR, periods.MONTH, periods.instant((2022, 12, 1)), periods.instant((2025, 11, 1)), 36], - [periods.YEAR, periods.YEAR, periods.instant((2022, 1, 1)), periods.instant((2024, 1, 1)), 3], + [day, day, Instant((2022, 12, 31)), Instant((2023, 1, 2)), 3], + [month, day, Instant((2022, 12, 31)), Instant((2023, 3, 30)), 90], + [month, month, Instant((2022, 12, 1)), Instant((2023, 2, 1)), 3], + [year, day, Instant((2022, 12, 31)), Instant((2025, 12, 30)), 1096], + [year, month, Instant((2022, 12, 1)), Instant((2025, 11, 1)), 36], + [year, year, Instant((2022, 1, 1)), Instant((2024, 1, 1)), 3], ]) def test_subperiods(instant, period_unit, unit, start, cease, count): """Returns the expected subperiods.""" - period = periods.period((period_unit, instant, 3)) + period = 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)) + assert subperiods[0] == Period((unit, start, 1)) + assert subperiods[-1] == Period((unit, cease, 1)) @pytest.mark.parametrize("period_unit, offset, unit, expected", [ - [periods.DAY, "first-of", periods.MONTH, periods.period((periods.DAY, periods.instant((2022, 12, 1)), 3))], - [periods.DAY, "first-of", periods.YEAR, periods.period((periods.DAY, periods.instant((2022, 1, 1)), 3))], - [periods.DAY, "last-of", periods.MONTH, periods.period((periods.DAY, periods.instant((2022, 12, 31)), 3))], - [periods.DAY, "last-of", periods.YEAR, periods.period((periods.DAY, periods.instant((2022, 12, 31)), 3))], - [periods.DAY, -3, periods.YEAR, periods.period((periods.DAY, periods.instant((2019, 12, 31)), 3))], - [periods.DAY, 1, periods.MONTH, periods.period((periods.DAY, periods.instant((2023, 1, 31)), 3))], - [periods.DAY, 3, periods.DAY, periods.period((periods.DAY, periods.instant((2023, 1, 3)), 3))], - [periods.MONTH, "first-of", periods.MONTH, periods.period((periods.MONTH, periods.instant((2022, 12, 1)), 3))], - [periods.MONTH, "first-of", periods.YEAR, periods.period((periods.MONTH, periods.instant((2022, 1, 1)), 3))], - [periods.MONTH, "last-of", periods.MONTH, periods.period((periods.MONTH, periods.instant((2022, 12, 31)), 3))], - [periods.MONTH, "last-of", periods.YEAR, periods.period((periods.MONTH, periods.instant((2022, 12, 31)), 3))], - [periods.MONTH, -3, periods.YEAR, periods.period((periods.MONTH, periods.instant((2019, 12, 31)), 3))], - [periods.MONTH, 1, periods.MONTH, periods.period((periods.MONTH, periods.instant((2023, 1, 31)), 3))], - [periods.MONTH, 3, periods.DAY, periods.period((periods.MONTH, periods.instant((2023, 1, 3)), 3))], - [periods.YEAR, "first-of", periods.MONTH, periods.period((periods.YEAR, periods.instant((2022, 12, 1)), 3))], - [periods.YEAR, "first-of", periods.YEAR, periods.period((periods.YEAR, periods.instant((2022, 1, 1)), 3))], - [periods.YEAR, "last-of", periods.MONTH, periods.period((periods.YEAR, periods.instant((2022, 12, 31)), 3))], - [periods.YEAR, "last-of", periods.YEAR, periods.period((periods.YEAR, periods.instant((2022, 12, 31)), 3))], - [periods.YEAR, -3, periods.YEAR, periods.period((periods.YEAR, periods.instant((2019, 12, 31)), 3))], - [periods.YEAR, 1, periods.MONTH, periods.period((periods.YEAR, periods.instant((2023, 1, 31)), 3))], - [periods.YEAR, 3, periods.DAY, periods.period((periods.YEAR, periods.instant((2023, 1, 3)), 3))], + [day, "first-of", month, Period((day, Instant((2022, 12, 1)), 3))], + [day, "first-of", year, Period((day, Instant((2022, 1, 1)), 3))], + [day, "last-of", month, Period((day, Instant((2022, 12, 31)), 3))], + [day, "last-of", year, Period((day, Instant((2022, 12, 31)), 3))], + [day, -3, year, Period((day, Instant((2019, 12, 31)), 3))], + [day, 1, month, Period((day, Instant((2023, 1, 31)), 3))], + [day, 3, day, Period((day, Instant((2023, 1, 3)), 3))], + [month, "first-of", month, Period((month, Instant((2022, 12, 1)), 3))], + [month, "first-of", year, Period((month, Instant((2022, 1, 1)), 3))], + [month, "last-of", month, Period((month, Instant((2022, 12, 31)), 3))], + [month, "last-of", year, Period((month, Instant((2022, 12, 31)), 3))], + [month, -3, year, Period((month, Instant((2019, 12, 31)), 3))], + [month, 1, month, Period((month, Instant((2023, 1, 31)), 3))], + [month, 3, day, Period((month, Instant((2023, 1, 3)), 3))], + [year, "first-of", month, Period((year, Instant((2022, 12, 1)), 3))], + [year, "first-of", year, Period((year, Instant((2022, 1, 1)), 3))], + [year, "last-of", month, Period((year, Instant((2022, 12, 31)), 3))], + [year, "last-of", year, Period((year, Instant((2022, 12, 31)), 3))], + [year, -3, year, Period((year, Instant((2019, 12, 31)), 3))], + [year, 1, month, Period((year, Instant((2023, 1, 31)), 3))], + [year, 3, day, Period((year, 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)) + period = Period((period_unit, instant, 3)) assert period.offset(offset, unit) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.MONTH, periods.instant((2012, 1, 3)), 3, 3], - [periods.MONTH, periods.instant((2012, 2, 3)), 1, 1], - [periods.MONTH, periods.instant((2022, 1, 3)), 3, 3], - [periods.MONTH, periods.instant((2022, 12, 1)), 1, 1], - [periods.YEAR, periods.instant((2012, 1, 1)), 1, 12], - [periods.YEAR, periods.instant((2022, 1, 1)), 2, 24], - [periods.YEAR, periods.instant((2022, 12, 1)), 1, 12], + [month, Instant((2012, 1, 3)), 3, 3], + [month, Instant((2012, 2, 3)), 1, 1], + [month, Instant((2022, 1, 3)), 3, 3], + [month, Instant((2022, 12, 1)), 1, 1], + [year, Instant((2012, 1, 1)), 1, 12], + [year, Instant((2022, 1, 1)), 2, 24], + [year, Instant((2022, 12, 1)), 1, 12], ]) def test_day_size_in_months(date_unit, instant, size, expected): """Returns the expected number of months.""" - period = periods.period((date_unit, instant, size)) + period = Period((date_unit, instant, size)) - assert period.count(periods.MONTH) == expected + assert period.count(month) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [periods.DAY, periods.instant((2022, 12, 31)), 1, 1], - [periods.DAY, periods.instant((2022, 12, 31)), 3, 3], - [periods.MONTH, periods.instant((2012, 1, 3)), 3, 31 + 29 + 31], - [periods.MONTH, periods.instant((2012, 2, 3)), 1, 29], - [periods.MONTH, periods.instant((2022, 1, 3)), 3, 31 + 28 + 31], - [periods.MONTH, periods.instant((2022, 12, 1)), 1, 31], - [periods.YEAR, periods.instant((2012, 1, 1)), 1, 366], - [periods.YEAR, periods.instant((2022, 1, 1)), 2, 730], - [periods.YEAR, periods.instant((2022, 12, 1)), 1, 365], + [day, Instant((2022, 12, 31)), 1, 1], + [day, Instant((2022, 12, 31)), 3, 3], + [month, Instant((2012, 1, 3)), 3, 31 + 29 + 31], + [month, Instant((2012, 2, 3)), 1, 29], + [month, Instant((2022, 1, 3)), 3, 31 + 28 + 31], + [month, Instant((2022, 12, 1)), 1, 31], + [year, Instant((2012, 1, 1)), 1, 366], + [year, Instant((2022, 1, 1)), 2, 730], + [year, 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)) + period = Period((date_unit, instant, size)) - assert period.count(periods.DAY) == expected + assert period.count(day) == expected @pytest.mark.parametrize("arg, expected", [ - ["1000", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], - ["1000-01", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 1))], - ["1000-01-01", periods.period((periods.DAY, periods.instant((1000, 1, 1)), 1))], - ["1004-02-29", periods.period((periods.DAY, periods.instant((1004, 2, 29)), 1))], - ["ETERNITY", periods.period((periods.ETERNITY, periods.instant((1, 1, 1)), 1))], - ["day:1000-01-01", periods.period((periods.DAY, periods.instant((1000, 1, 1)), 1))], - ["day:1000-01-01:3", periods.period((periods.DAY, periods.instant((1000, 1, 1)), 3))], - ["eternity", periods.period((periods.ETERNITY, periods.instant((1, 1, 1)), 1))], - ["month:1000-01", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 1))], - ["month:1000-01-01", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 1))], - ["month:1000-01-01:3", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 3))], - ["month:1000-01:3", periods.period((periods.MONTH, periods.instant((1000, 1, 1)), 3))], - ["year:1000", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], - ["year:1000-01", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], - ["year:1000-01-01", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], - ["year:1000-01-01:3", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 3))], - ["year:1000-01:3", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 3))], - ["year:1000:3", periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 3))], - [1000, periods.period((periods.YEAR, periods.instant((1000, 1, 1)), 1))], - [periods.ETERNITY, periods.period((periods.ETERNITY, periods.instant((1, 1, 1)), 1))], - [periods.instant((1, 1, 1)), periods.period((periods.DAY, periods.instant((1, 1, 1)), 1))], - [periods.period((periods.DAY, periods.instant((1, 1, 1)), 365)), periods.period((periods.DAY, periods.instant((1, 1, 1)), 365))], + ["1000", Period((year, Instant((1000, 1, 1)), 1))], + ["1000-01", Period((month, Instant((1000, 1, 1)), 1))], + ["1000-01-01", Period((day, Instant((1000, 1, 1)), 1))], + ["1004-02-29", Period((day, Instant((1004, 2, 29)), 1))], + ["ETERNITY", Period((eternity, Instant((1, 1, 1)), 1))], + ["day:1000-01-01", Period((day, Instant((1000, 1, 1)), 1))], + ["day:1000-01-01:3", Period((day, Instant((1000, 1, 1)), 3))], + ["eternity", Period((eternity, Instant((1, 1, 1)), 1))], + ["month:1000-01", Period((month, Instant((1000, 1, 1)), 1))], + ["month:1000-01-01", Period((month, Instant((1000, 1, 1)), 1))], + ["month:1000-01-01:3", Period((month, Instant((1000, 1, 1)), 3))], + ["month:1000-01:3", Period((month, Instant((1000, 1, 1)), 3))], + ["year:1000", Period((year, Instant((1000, 1, 1)), 1))], + ["year:1000-01", Period((year, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01", Period((year, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01:3", Period((year, Instant((1000, 1, 1)), 3))], + ["year:1000-01:3", Period((year, Instant((1000, 1, 1)), 3))], + ["year:1000:3", Period((year, Instant((1000, 1, 1)), 3))], + [1000, Period((year, Instant((1000, 1, 1)), 1))], + [eternity, Period((eternity, Instant((1, 1, 1)), 1))], + [Instant((1, 1, 1)), Period((day, Instant((1, 1, 1)), 1))], + [Period((day, Instant((1, 1, 1)), 365)), Period((day, Instant((1, 1, 1)), 365))], ]) def test_build(arg, expected): """Returns the expected ``Period``.""" - assert periods.build(arg) == expected + assert Period.build(arg) == expected @pytest.mark.parametrize("arg, error", [ @@ -182,10 +184,10 @@ def test_build(arg, expected): ["month:1000:1", ValueError], [None, TypeError], [datetime.date(1, 1, 1), ValueError], - [periods.YEAR, TypeError], + [year, TypeError], ]) def test_build_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" with pytest.raises(error): - periods.build(arg) + Period.build(arg) From fc644f7545303352909eed0a0bc6fd3f7d2b2afa Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 18 Dec 2022 12:04:07 +0100 Subject: [PATCH 72/93] Use named isoformat attrs --- openfisca_core/periods/period_.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 9bcbb7ff42..7cbea6981b 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -600,7 +600,8 @@ def build(cls, value: Any) -> Period: part = ISOFormat.parse(value) if part is not None: - return cls((DateUnit(part.unit), Instant((part[1:-1])), 1)) + start = Instant((part.year, part.month, part.day)) + return cls((DateUnit(part.unit), start, 1)) # Complex periods must have a ':' in their strings if ":" not in value: @@ -652,4 +653,6 @@ def build(cls, value: Any) -> Period: if part.unit > unit: raise PeriodFormatError(value) - return cls((unit, Instant((part[1:-1])), size)) + start = Instant((part.year, part.month, part.day)) + + return cls((unit, start, size)) From cbcceab91787a72cc10a409734f0f4c944dce604 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 18 Dec 2022 12:10:38 +0100 Subject: [PATCH 73/93] Simplify iso parser --- openfisca_core/periods/_parsers.py | 14 +++++++------- openfisca_core/periods/instant_.py | 2 +- openfisca_core/periods/tests/test__parsers.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index bec70c51d0..046668aa08 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -10,9 +10,6 @@ class ISOFormat(NamedTuple): """An implementation of the `parse` protocol.""" - #: The unit of the parsed period, in binary. - unit: int - #: The year of the parsed period. year: int @@ -22,6 +19,9 @@ class ISOFormat(NamedTuple): #: The month of the parsed period. day: int + #: The unit of the parsed period, in binary. + unit: int + #: The number of fragments in the parsed period. shape: int @@ -44,13 +44,13 @@ def parse(cls, value: str) -> ISOFormat | None: >>> ISOFormat.parse("ETERNITY") >>> ISOFormat.parse("2022") - ISOFormat(unit=4, year=2022, month=1, day=1, shape=1) + ISOFormat(year=2022, month=1, day=1, unit=4, shape=1) >>> ISOFormat.parse("2022-02") - ISOFormat(unit=2, year=2022, month=2, day=1, shape=2) + ISOFormat(year=2022, month=2, day=1, unit=2, shape=2) >>> ISOFormat.parse("2022-02-13") - ISOFormat(unit=1, year=2022, month=2, day=13, shape=3) + ISOFormat(year=2022, month=2, day=13, unit=1, shape=3) .. versionadded:: 39.0.0 @@ -84,4 +84,4 @@ def parse(cls, value: str) -> ISOFormat | None: unit = pow(2, 3 - shape) # We build the corresponding ISOFormat object - return cls(unit, date.year, date.month, date.day, shape) + return cls(date.year, date.month, date.day, unit, shape) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index dfe5abfc35..2ba88cf151 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -300,6 +300,6 @@ def build(cls, value: Any) -> Instant: return cls((instant[0], instant[1], 1)) if len(instant) == 5: - return cls(instant[1:-1]) + return cls((instant.year, instant.month, instant.day)) return cls(instant) diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test__parsers.py index d16924bf47..eda07d0640 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test__parsers.py @@ -5,9 +5,9 @@ @pytest.mark.parametrize("arg, expected", [ ["1", None], - ["1000", (4, 1000, 1, 1, 1)], - ["1000-01", (2, 1000, 1, 1, 2)], - ["1000-01-01", (1, 1000, 1, 1, 3)], + ["1000", (1000, 1, 1, 4, 1)], + ["1000-01", (1000, 1, 1, 2, 2)], + ["1000-01-01", (1000, 1, 1, 1, 3)], ["1000-01-1", None], ["1000-01-99", None], ["1000-1", None], From f84afbfb9c04cfb47b40ec27b90f53cd7f8e4888 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 18 Dec 2022 15:06:40 +0100 Subject: [PATCH 74/93] Fix type error in instant --- openfisca_core/periods/_parsers.py | 151 +++++++++++++++--- openfisca_core/periods/instant_.py | 50 ++---- openfisca_core/periods/period_.py | 4 +- openfisca_core/periods/tests/test__parsers.py | 115 ++++++++++++- openfisca_core/periods/tests/test_instant.py | 6 +- 5 files changed, 265 insertions(+), 61 deletions(-) diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index 046668aa08..1e34d5d88a 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import NamedTuple +from typing import NamedTuple, Sequence import pendulum from pendulum.datetime import Date @@ -26,7 +26,50 @@ class ISOFormat(NamedTuple): shape: int @classmethod - def parse(cls, value: str) -> ISOFormat | None: + def fromint(cls, value: int) -> ISOFormat | None: + """Parse an int respecting the ISO format. + + Args: + value: The integer to parse. + + Returns: + An ISOFormat object if ``value`` is valid. + None otherwise. + + Examples: + >>> ISOFormat.fromint(1) + ISOFormat(year=1, month=1, day=1, unit=4, shape=1) + + >>> ISOFormat.fromint(2023) + ISOFormat(year=2023, month=1, day=1, unit=4, shape=1) + + >>> ISOFormat.fromint(-1) + + >>> ISOFormat.fromint("2023") + + >>> ISOFormat.fromint(20231231) + + .. versionadded:: 39.0.0 + + """ + + if not isinstance(value, int): + return None + + if not 1 <= len(str(value)) <= 4: + return None + + try: + if not 1 <= int(str(value)[:4]) < 10000: + return None + + except ValueError: + return None + + return cls(value, 1, 1, 4, 1) + + @classmethod + def fromstr(cls, value: str) -> ISOFormat | None: """Parse strings respecting the ISO format. Args: @@ -36,46 +79,47 @@ def parse(cls, value: str) -> ISOFormat | None: An ISOFormat object if ``value`` is valid. None if ``value`` is not valid. - Raises: - AttributeError: When arguments are invalid, like ``"-1"``. - ValueError: When values are invalid, like ``"2022-32-13"``. - Examples: - >>> ISOFormat.parse("ETERNITY") - - >>> ISOFormat.parse("2022") + >>> ISOFormat.fromstr("2022") ISOFormat(year=2022, month=1, day=1, unit=4, shape=1) - >>> ISOFormat.parse("2022-02") + >>> ISOFormat.fromstr("2022-02") ISOFormat(year=2022, month=2, day=1, unit=2, shape=2) - >>> ISOFormat.parse("2022-02-13") + >>> ISOFormat.fromstr("2022-02-13") ISOFormat(year=2022, month=2, day=13, unit=1, shape=3) + >>> ISOFormat.fromstr(1000) + + >>> ISOFormat.fromstr("ETERNITY") + .. versionadded:: 39.0.0 """ - # If it's a complex period, next! - if len(value.split(":")) != 1: + if not isinstance(value, str): return None - # Check for a non-empty string. - if not value and not isinstance(value, str): - raise AttributeError + if not value: + return None + + # If it is a complex value, next! + if len(value.split(":")) != 1: + return None # If it's negative period, next! - if value[0] == "-" or len(value.split(":")) != 1: + if value[0] == "-": raise ValueError try: - date = pendulum.parse(value, exact = True) + # We parse the date + date = pendulum.parse(value, exact = True, strict = True) except ParserError: return None if not isinstance(date, Date): - raise ValueError + return None # We get the shape of the string (e.g. "2012-02" = 2) shape = len(value.split("-")) @@ -85,3 +129,72 @@ def parse(cls, value: str) -> ISOFormat | None: # We build the corresponding ISOFormat object return cls(date.year, date.month, date.day, unit, shape) + + + @classmethod + def fromseq(cls, value: Sequence[int]) -> ISOFormat | None: + """Parse a sequence of ints respecting the ISO format. + + Args: + value: A sequence of ints such as [2012, 3, 13]. + + Returns: + An ISOFormat object if ``value`` is valid. + None if ``value`` is not valid. + + Examples: + >>> ISOFormat.fromseq([2022]) + ISOFormat(year=2022, month=1, day=1, unit=4, shape=1) + + >>> ISOFormat.fromseq([2022, 1]) + ISOFormat(year=2022, month=1, day=1, unit=2, shape=2) + + >>> ISOFormat.fromseq([2022, 1, 1]) + ISOFormat(year=2022, month=1, day=1, unit=1, shape=3) + + >>> ISOFormat.fromseq([-2022, 1, 1]) + + >>> ISOFormat.fromseq([2022, 13, 1]) + + >>> ISOFormat.fromseq([2022, 1, 32]) + + .. versionadded:: 39.0.0 + + """ + + if not isinstance(value, (list, tuple)): + return None + + if not value: + return None + + if not 1 <= len(value) <= 3: + return None + + if not all(isinstance(unit, int) for unit in value): + return None + + if not all(unit == abs(unit) for unit in value): + return None + + # We get the shape of the string (e.g. "2012-02" = 2) + shape = len(value) + + # We get the unit from the shape (e.g. 2 = "month") + unit = pow(2, 3 - shape) + + while len(value) < 3: + value = (*value, 1) + + try: + # We parse the date + date = pendulum.date(*value) + + except ValueError: + return None + + if not isinstance(date, Date): + return None + + # We build the corresponding ISOFormat object + return cls(date.year, date.month, date.day, unit, shape) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 2ba88cf151..372f89e4bd 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Tuple +from typing import Any, Sequence, Tuple import calendar import datetime @@ -186,6 +186,9 @@ def offset(self, offset: str | int, unit: int) -> Instant: year, month, day = self + if not isinstance(unit, DateUnit): + raise DateUnitValueError(unit) + if not unit & DateUnit.isoformat: raise DateUnitValueError(unit) @@ -252,54 +255,33 @@ def build(cls, value: Any) -> Instant: >>> Instant.build(period) Traceback (most recent call last): - TypeError: int() argument must be a string, a bytes-like object ... + InstantFormatError: 'year:2021-09' is not a valid instant. .. versionadded:: 39.0.0 """ - instant: Tuple[int, ...] | ISOFormat | None - - if value is None or isinstance(value, DateUnit): - raise InstantTypeError(value) + instant: ISOFormat | None if isinstance(value, Instant): return value - if isinstance(value, str) and not INSTANT_PATTERN.match(value): - raise InstantFormatError(value) - - if isinstance(value, str) and len(value.split("-")) > 3: - raise InstantValueError(value) + if isinstance(value, datetime.date): + return cls((value.year, value.month, value.day)) - if isinstance(value, str): - instant = ISOFormat.parse(value) + if isinstance(value, int): + instant = ISOFormat.fromint(value) - elif isinstance(value, datetime.date): - instant = value.year, value.month, value.day + elif isinstance(value, str): + instant = ISOFormat.fromstr(value) - elif isinstance(value, int): - instant = value, 1, 1 - - elif isinstance(value, (dict, set)): - raise InstantValueError(value) - - elif isinstance(value, (tuple, list)) and not 1 <= len(value) <= 3: - raise InstantValueError(value) + elif isinstance(value, (list, tuple)): + instant = ISOFormat.fromseq(value) else: - instant = tuple(int(value) for value in tuple(value)) + raise InstantTypeError(value) if instant is None: raise InstantFormatError(value) - if len(instant) == 1: - return cls((instant[0], 1, 1)) - - if len(instant) == 2: - return cls((instant[0], instant[1], 1)) - - if len(instant) == 5: - return cls((instant.year, instant.month, instant.day)) - - return cls(instant) + return cls((instant.year, instant.month, instant.day)) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 7cbea6981b..46185309ef 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -597,7 +597,7 @@ def build(cls, value: Any) -> Period: raise PeriodFormatError(value) # Try to parse as a simple period - part = ISOFormat.parse(value) + part = ISOFormat.fromstr(value) if part is not None: start = Instant((part.year, part.month, part.day)) @@ -626,7 +626,7 @@ def build(cls, value: Any) -> Period: raise PeriodFormatError(value) # Middle component must be a valid ISO period - part = ISOFormat.parse(date) + part = ISOFormat.fromstr(date) if part is None: raise PeriodFormatError(value) diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test__parsers.py index eda07d0640..9f377265fa 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test__parsers.py @@ -1,6 +1,48 @@ import pytest -from openfisca_core.periods import ISOFormat +from openfisca_core.periods import DateUnit, ISOFormat + + +@pytest.mark.parametrize("arg, expected", [ + ["1", None], + ["1000", None], + ["1000-01", None], + ["1000-01-01", None], + ["1000-01-1", None], + ["1000-01-99", None], + ["1000-1", None], + ["1000-1-1", None], + ["999", None], + ["eternity", None], + ["first-of", None], + ["year:2021:7", None], + [(1, 1), None], + [(1, 1, 1), None], + [(1, 1, 1, 1), None], + [(1,), None], + [(2022, 1), None], + [(2022, 1, 1), None], + [(2022, 12), None], + [(2022, 12, 1), None], + [(2022, 12, 31), None], + [(2022, 12, 32), None], + [(2022, 13), None], + [(2022, 13, 31), None], + [(2022,), None], + [1, (1, 1, 1, 4, 1)], + [1., None], + [1000, (1000, 1, 1, 4, 1)], + [1000., None], + [DateUnit.YEAR, None], + [{1, 1, 1, 1}, None], + [{1, 1, 1}, None], + [{1, 1}, None], + [{1, }, None], + ]) +def test_parse_iso_format_from_int(arg, expected): + """Returns an ``ISOFormat`` when given a valid ISO format int.""" + + assert ISOFormat.fromint(arg) == expected @pytest.mark.parametrize("arg, expected", [ @@ -13,8 +55,75 @@ ["1000-1", None], ["1000-1-1", None], ["999", None], + ["eternity", None], + ["first-of", None], + ["year:2021:7", None], + [(1, 1), None], + [(1, 1, 1), None], + [(1, 1, 1, 1), None], + [(1,), None], + [(2022, 1), None], + [(2022, 1, 1), None], + [(2022, 12), None], + [(2022, 12, 1), None], + [(2022, 12, 31), None], + [(2022, 12, 32), None], + [(2022, 13), None], + [(2022, 13, 31), None], + [(2022,), None], + [1, None], + [1., None], + [1000, None], + [1000., None], + [DateUnit.YEAR, None], + [{1, 1, 1, 1}, None], + [{1, 1, 1}, None], + [{1, 1}, None], + [{1, }, None], ]) -def test_parse_iso_format(arg, expected): +def test_parse_iso_format_from_str(arg, expected): """Returns an ``ISOFormat`` when given a valid ISO format string.""" - assert ISOFormat.parse(arg) == expected + assert ISOFormat.fromstr(arg) == expected + + +@pytest.mark.parametrize("arg, expected", [ + ["1", None], + ["1000", None], + ["1000-01", None], + ["1000-01-01", None], + ["1000-01-1", None], + ["1000-01-99", None], + ["1000-1", None], + ["1000-1-1", None], + ["999", None], + ["eternity", None], + ["first-of", None], + ["year:2021:7", None], + [(1, 1), (1, 1, 1, 2, 2)], + [(1, 1, 1), (1, 1, 1, 1, 3)], + [(1, 1, 1, 1), None], + [(1,), (1, 1, 1, 4, 1)], + [(2022, 1), (2022, 1, 1, 2, 2)], + [(2022, 1, 1), (2022, 1, 1, 1, 3)], + [(2022, 12), (2022, 12, 1, 2, 2)], + [(2022, 12, 1), (2022, 12, 1, 1, 3)], + [(2022, 12, 31), (2022, 12, 31, 1, 3)], + [(2022, 12, 32), None], + [(2022, 13), None], + [(2022, 13, 31), None], + [(2022,), (2022, 1, 1, 4, 1)], + [1, None], + [1., None], + [1000, None], + [1000., None], + [DateUnit.YEAR, None], + [{1, 1, 1, 1}, None], + [{1, 1, 1}, None], + [{1, 1}, None], + [{1}, None], + ]) +def test_parse_iso_format_from_seq(arg, expected): + """Returns an ``ISOFormat`` when given a valid ISO format sequence.""" + + assert ISOFormat.fromseq(arg) == expected diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index b5010c2eb2..efaba07db0 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -65,9 +65,9 @@ def test_build_instant(arg, expected): ["year:1000-01-01:1", ValueError], ["year:1000-01-01:3", ValueError], [None, TypeError], - [eternity, TypeError], - [year, TypeError], - [Period((day, Instant((1, 1, 1)), 365)), TypeError], + [eternity, ValueError], + [year, ValueError], + [Period((day, Instant((1, 1, 1)), 365)), ValueError], ]) def test_build_instant_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" From 97de3eebc462bd739f7bf6b9c4731f27da14f175 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 18 Dec 2022 15:46:56 +0100 Subject: [PATCH 75/93] Update CHANGELOG --- CHANGELOG.md | 57 ++++++++++++++----- .../data_storage/in_memory_storage.py | 12 ++-- .../data_storage/on_disk_storage.py | 14 ++--- openfisca_core/holders/holder.py | 2 +- openfisca_core/parameters/helpers.py | 2 +- openfisca_core/parameters/parameter.py | 4 +- openfisca_core/periods/__init__.py | 8 ++- openfisca_core/periods/_config.py | 9 --- openfisca_core/periods/_parsers.py | 3 +- openfisca_core/periods/instant_.py | 9 +-- openfisca_core/periods/period_.py | 2 +- openfisca_core/periods/tests/test_instant.py | 2 +- openfisca_core/periods/tests/test_period.py | 2 +- openfisca_core/populations/population.py | 2 +- .../scripts/measure_performances.py | 2 +- openfisca_core/simulations/simulation.py | 10 ++-- .../simulations/simulation_builder.py | 8 +-- openfisca_core/variables/variable.py | 2 +- tests/core/test_countries.py | 2 +- tests/core/test_cycles.py | 2 +- tests/core/test_holders.py | 30 +++++----- tests/core/test_opt_out_cache.py | 2 +- tests/core/test_reforms.py | 34 +++++------ tests/core/variables/test_annualize.py | 8 +-- 24 files changed, 124 insertions(+), 104 deletions(-) delete mode 100644 openfisca_core/periods/_config.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 00ff9349a1..9f5b64ce2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,31 +4,60 @@ #### Breaking changes +##### Renames + +- Rename `periods.period.get_subperiods` to `periods.period.subperiods`. +- Rename `periods.instant` to `periods.instant.build`. +- Rename `periods.period` to `periods.period.build`. + +##### Deprecations + +- Deprecate `INSTANT_PATTERN` + - The feature is now provided by `periods.isoformat.fromstr` - Deprecate `instant_date`. + - The feature is now provided by `periods.instant.date`. +- Deprecate `periods.{unit_weight, unit_weights, key_period_size}`. + - These features are now provided by `periods.dateunit`. - Deprecate `periods.intersect`. -- Deprecate `periods.unit_weight`. -- Deprecate `periods.unit_weights`. -- Deprecate `periods.key_period_size`. -- 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 `Instant.build`. -- Rename `period` to `Period.build`. -- Transform `Instant.date` from property to method. + - The feature has no replacement. +- Make `periods.parse_period` stricter. + - For example `2022-1` now fails. +- Refactor `periods.period.contains` as `__contains__`. + - For example `subperiod in period` is now possible. + +##### Structural changes + - Transform `Period.date` from property to method. -- Simplify reference periods. + - Now it has to be used as `period.date()` (note the parenthesis). +- Transform `Instant.date` from property to method. + - Now it has to be used as `instant.date()` (note the parenthesis). +- Rationalise the reference periods. + - Before, there was a definite list of reference periods. For example, + `period.first_month` or `period.n_2`. + - This has been simplified to allow users to build their own: + - `period.ago(unit: DateUnit, size: int = 1) -> Period`. + - `period.last(unit: DateUnit, size: int = 1) -> Period`. + - `period.this(unit: DateUnit) -> Period`. +- Rationalise date units. + - Before, usage of "month", YEAR, and so on was fairly inconsistent, and + providing a perfect hotbed for bugs to breed. + - This has been fixed by introducing a new `dateunit` module, which + provides a single source of truth for all date units. + - Note that if you used `periods.YEAR` and the like, there is nothing to + change in your code. + - However, strings like `"year"` or `"ETERNITY"` are no longer allowed (in + fact, date unit are int enums an no longer strings). #### Technical changes - Add typing to `openfisca_core.periods`. -- Document `openfisca_core.periods`. - Fix `openfisca_core.periods` doctests. +- Document `openfisca_core.periods`. #### Bug fixes -- Fixes impossible `last-of` and `first-of` offsets. -- Fixes incoherent dates, -- Fixes several race conditions, +- Fixes incoherent dates. +- Fixes several race conditions. # 38.0.0 [#989](https://github.com/openfisca/openfisca-core/pull/989) diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py index 5b479474ee..8d391d5dcf 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.build(periods.ETERNITY) - period = periods.build(period) + period = periods.period.build(periods.ETERNITY) + period = periods.period.build(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.build(periods.ETERNITY) - period = periods.build(period) + period = periods.period.build(periods.ETERNITY) + period = periods.period.build(period) self._arrays[period] = value @@ -35,8 +35,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.build(periods.ETERNITY) - period = periods.build(period) + period = periods.period.build(periods.ETERNITY) + period = periods.period.build(period) self._arrays = { period_item: value diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py index cfcab32cc1..04dd45d720 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -28,8 +28,8 @@ def _decode_file(self, file): def get(self, period): if self.is_eternal: - period = periods.build(periods.ETERNITY) - period = periods.build(period) + period = periods.period.build(periods.ETERNITY) + period = periods.period.build(period) values = self._files.get(period) if values is None: @@ -38,8 +38,8 @@ def get(self, period): def put(self, value, period): if self.is_eternal: - period = periods.build(periods.ETERNITY) - period = periods.build(period) + period = periods.period.build(periods.ETERNITY) + period = periods.period.build(period) filename = str(period) path = os.path.join(self.storage_dir, filename) + '.npy' @@ -55,8 +55,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.build(periods.ETERNITY) - period = periods.build(period) + period = periods.period.build(periods.ETERNITY) + period = periods.period.build(period) if period is not None: self._files = { @@ -76,7 +76,7 @@ def restore(self): continue path = os.path.join(self.storage_dir, filename) filename_core = filename.rsplit('.', 1)[0] - period = periods.build(filename_core) + period = periods.period.build(filename_core) files[period] = path def __del__(self): diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index 19c3725df2..368b234add 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -206,7 +206,7 @@ def set_input( """ - period = periods.build(period) + period = periods.period.build(period) if period is None: raise ValueError(f"Invalid period value: {period}") diff --git a/openfisca_core/parameters/helpers.py b/openfisca_core/parameters/helpers.py index 75d5a18b73..9426de638c 100644 --- a/openfisca_core/parameters/helpers.py +++ b/openfisca_core/parameters/helpers.py @@ -63,7 +63,7 @@ def _parse_child(child_name, child, child_path): return parameters.Parameter(child_name, child, child_path) elif 'brackets' in child: return parameters.ParameterScale(child_name, child, child_path) - elif isinstance(child, dict) and all([periods.INSTANT_PATTERN.match(str(key)) for key in child.keys()]): + elif isinstance(child, dict) and all([periods.isoformat.fromstr(str(key)) for key in child.keys()]): return parameters.Parameter(child_name, child, child_path) else: return parameters.ParameterNode(child_name, data = child, file_path = child_path) diff --git a/openfisca_core/parameters/parameter.py b/openfisca_core/parameters/parameter.py index 8c038c611b..25bd7ce22e 100644 --- a/openfisca_core/parameters/parameter.py +++ b/openfisca_core/parameters/parameter.py @@ -73,7 +73,7 @@ def __init__(self, name: str, data: dict, file_path: Optional[str] = None) -> No values_list = [] for instant_str in instants: - if not periods.INSTANT_PATTERN.match(instant_str): + if periods.isoformat.fromstr(instant_str) is None: raise ParameterParsingError( "Invalid property '{}' in '{}'. Properties must be valid YYYY-MM-DD instants, such as 2017-01-15." .format(instant_str, self.name), @@ -120,7 +120,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.build(period) + period = periods.period.build(period) start = period.start stop = period.stop if start is None: diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 2d8a715466..76f3add97e 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -27,9 +27,8 @@ """ -from ._parsers import ISOFormat from ._date_unit import DateUnit -from ._config import INSTANT_PATTERN +from ._parsers import ISOFormat from .instant_ import Instant from .period_ import Period @@ -41,3 +40,8 @@ instant = Instant period = Period isoformat = ISOFormat + +# Deprecated + +setattr(Period, "this_year", property(lambda self: self.this(YEAR))) # noqa: B010 +setattr(Period, "first_month", property(lambda self: self.this(MONTH))) # noqa: B010 diff --git a/openfisca_core/periods/_config.py b/openfisca_core/periods/_config.py deleted file mode 100644 index f3d3cf2b2a..0000000000 --- a/openfisca_core/periods/_config.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import annotations - -from typing import Pattern - -import re - -# 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]))?$") diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index 1e34d5d88a..4f89c1bde1 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -109,7 +109,7 @@ def fromstr(cls, value: str) -> ISOFormat | None: # If it's negative period, next! if value[0] == "-": - raise ValueError + return None try: # We parse the date @@ -130,7 +130,6 @@ def fromstr(cls, value: str) -> ISOFormat | None: # We build the corresponding ISOFormat object return cls(date.year, date.month, date.day, unit, shape) - @classmethod def fromseq(cls, value: Sequence[int]) -> ISOFormat | None: """Parse a sequence of ints respecting the ISO format. diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 372f89e4bd..c483a6bac6 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Sequence, Tuple +from typing import Any, Tuple import calendar import datetime @@ -10,16 +10,14 @@ import pendulum from pendulum.datetime import Date -from ._config import INSTANT_PATTERN +from ._date_unit import DateUnit from ._errors import ( DateUnitValueError, InstantFormatError, InstantTypeError, - InstantValueError, OffsetTypeError, ) from ._parsers import ISOFormat -from ._date_unit import DateUnit from .typing import Add, Plural DAY, MONTH, YEAR, _ = tuple(DateUnit) @@ -229,7 +227,6 @@ def build(cls, value: Any) -> Instant: Raises: InstantFormatError: When ``value`` is invalid, like "2021-32-13". - InstantValueError: When the length of ``value`` is out of range. InstantTypeError: When ``value`` is None. Examples: @@ -255,7 +252,7 @@ def build(cls, value: Any) -> Instant: >>> Instant.build(period) Traceback (most recent call last): - InstantFormatError: 'year:2021-09' is not a valid instant. + openfisca_core.periods._errors.InstantFormatError: 'year:2021-09... .. versionadded:: 39.0.0 diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 46185309ef..276aaeec43 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -6,9 +6,9 @@ import inflect +from ._date_unit import DateUnit from ._errors import DateUnitValueError, PeriodFormatError, PeriodTypeError from ._parsers import ISOFormat -from ._date_unit import DateUnit from .instant_ import Instant from .typing import Plural diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index efaba07db0..ba1682144a 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -2,7 +2,7 @@ import pytest -from openfisca_core.periods import Instant, Period, DateUnit +from openfisca_core.periods import DateUnit, Instant, Period day, month, year, eternity = DateUnit diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 4339b173db..a3477e3ea1 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -2,7 +2,7 @@ import pytest -from openfisca_core.periods import DateUnit, Period, Instant +from openfisca_core.periods import DateUnit, Instant, Period day, month, year, eternity = DateUnit diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index b79c34760d..708d7cf88d 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -111,7 +111,7 @@ def __call__( calculate: Calculate = Calculate( variable = variable_name, - period = periods.build(period), + period = periods.period.build(period), option = options, ) diff --git a/openfisca_core/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index a9e1528290..4b8459420b 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -186,7 +186,7 @@ def formula(self, simulation, period): @timeit def check_revenu_disponible(year, city_code, expected_revenu_disponible): - simulation = simulations.Simulation(period = periods.build(year), tax_benefit_system = tax_benefit_system) + simulation = simulations.Simulation(period = periods.period.build(year), tax_benefit_system = tax_benefit_system) famille = simulation.populations["famille"] famille.count = 3 famille.roles_count = 2 diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 41f11973e7..db4ab9fd85 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -103,7 +103,7 @@ def calculate(self, variable_name: str, period): """Calculate ``variable_name`` for ``period``.""" if period is not None and not isinstance(period, periods.period): - period = periods.build(period) + period = periods.period.build(period) self.tracer.record_calculation_start(variable_name, period) @@ -175,7 +175,7 @@ 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, periods.period): - period = periods.build(period) + period = periods.period.build(period) # Check that the requested period matches definition_period if variable.definition_period > period.unit: @@ -204,7 +204,7 @@ 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, periods.period): - period = periods.build(period) + period = periods.period.build(period) # Check that the requested period matches definition_period if variable.definition_period != periods.YEAR: @@ -352,7 +352,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, periods.period): - period = periods.build(period) + period = periods.period.build(period) return self.get_holder(variable_name).get_array(period) def get_holder(self, variable_name: str): @@ -445,7 +445,7 @@ def set_input(self, variable_name: str, period, value): if variable is None: raise VariableNotFoundError(variable_name, self.tax_benefit_system) - period = periods.build(period) + period = periods.period.build(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 99e0ee7673..b10cd68c27 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -325,7 +325,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.build(period_str)) + self.default_period = str(periods.period.build(period_str)) def get_input(self, variable, period_str): if variable not in self.input_buffer: @@ -368,7 +368,7 @@ def init_variable_values(self, entity, instance_object, instance_id): for period_str, value in variable_values.items(): try: - periods.build(period_str) + periods.period.build(period_str) except ValueError as e: raise SituationParsingError(path_in_json, e.args[0]) variable = entity.get_variable(variable_name) @@ -393,7 +393,7 @@ def add_variable_value(self, entity, variable, instance_index, instance_id, peri array[instance_index] = value - self.input_buffer[variable.name][str(periods.build(period_str))] = array + self.input_buffer[variable.name][str(periods.period.build(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 +411,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.build(period_str) for period_str in self.input_buffer[variable_name].keys()] + unsorted_periods = [periods.period.build(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 = lambda period: f"{period.unit}_{period.size}") diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 06b850472d..21b757cc30 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -335,7 +335,7 @@ def get_formula( else: try: - instant = periods.build(period).start + instant = periods.period.build(period).start except ValueError: instant = periods.instant.build(period) diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index 1a91b56b9a..16219c7257 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -5,7 +5,7 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable -PERIOD = periods.build("2016-01") +PERIOD = periods.period.build("2016-01") @pytest.mark.parametrize("simulation", [({"salary": 2000}, PERIOD)], indirect = True) diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index 7ee4c64a15..b7aee1e852 100644 --- a/tests/core/test_cycles.py +++ b/tests/core/test_cycles.py @@ -10,7 +10,7 @@ @pytest.fixture def reference_period(): - return periods.build('2013-01') + return periods.period.build('2013-01') @pytest.fixture diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index 5f7bc827de..bad6c4a5d4 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -26,7 +26,7 @@ def couple(tax_benefit_system): build_from_entities(tax_benefit_system, situation_examples.couple) -period = periods.build('2017-12') +period = periods.period.build('2017-12') def test_set_input_enum_string(couple): @@ -89,7 +89,7 @@ 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.build(periods.ETERNITY), value) + holder.set_input(periods.period.build(periods.ETERNITY), value) assert holder.get_array(None) == value assert holder.get_array(periods.ETERNITY) == value assert holder.get_array('2016-01') == value @@ -98,8 +98,8 @@ def test_permanent_variable_filled(single): def test_delete_arrays(single): simulation = single salary_holder = simulation.person.get_holder('salary') - salary_holder.set_input(periods.build(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.build(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.period.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.period.build(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 +109,7 @@ def test_delete_arrays(single): salary_array = simulation.get_array('salary', '2018-01') assert salary_array is None - salary_holder.set_input(periods.build(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.period.build(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -119,7 +119,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.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.period.build(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 +132,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.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.period.build(2017), numpy.asarray([30000])) simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-02') @@ -147,7 +147,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.build(2019), 'owner') + holders.set_input_dispatch_by_period(holder, periods.period.build(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 +159,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.build(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.build(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.period.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.period.build(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.build(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.period.build(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -172,7 +172,7 @@ def test_delete_arrays_on_disk(single): def test_cache_disk(couple): simulation = couple simulation.memory_config = force_storage_on_disk - month = periods.build('2017-01') + month = periods.period.build('2017-01') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -183,8 +183,8 @@ def test_cache_disk(couple): def test_known_periods(couple): simulation = couple simulation.memory_config = force_storage_on_disk - month = periods.build('2017-01') - month_2 = periods.build('2017-02') + month = periods.period.build('2017-01') + month_2 = periods.period.build('2017-02') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -196,7 +196,7 @@ def test_known_periods(couple): def test_cache_enum_on_disk(single): simulation = single simulation.memory_config = force_storage_on_disk - month = periods.build('2017-01') + month = periods.period.build('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 7a9ad4fa85..4ec9251224 100644 --- a/tests/core/test_opt_out_cache.py +++ b/tests/core/test_opt_out_cache.py @@ -7,7 +7,7 @@ from openfisca_core.variables import Variable -PERIOD = periods.build("2016-01") +PERIOD = periods.period.build("2016-01") class input(Variable): diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index 877a212b95..aac5aaab49 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -124,23 +124,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.build(2013).start, - periods.build(2013).stop, + periods.period.build(2013).start, + periods.period.build(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.build(2015).start, - periods.build(2015).stop, + periods.period.build(2015).start, + periods.period.build(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.build(2013).start, + periods.period.build(2013).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2013-01-01": {'value': 1.0}}), @@ -148,15 +148,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.build(2011).start, - periods.build(2011).stop, + periods.period.build(2011).start, + periods.period.build(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.build(2015).start, + periods.period.build(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 +164,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.build(2014).start, - periods.build(2014).stop, + periods.period.build(2014).start, + periods.period.build(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.build(2014).start, + periods.period.build(2014).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 0.055}, "2014-01-01": {'value': 1.0}}), @@ -180,23 +180,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.build(2005).start, - periods.build(2005).stop, + periods.period.build(2005).start, + periods.period.build(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.build(2003).start, - periods.build(2003).stop, + periods.period.build(2003).start, + periods.period.build(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.build(2005).start, + periods.period.build(2005).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2005-01-01": {'value': 1.0}}), @@ -204,7 +204,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.build(2006).start, + periods.period.build(2006).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 1.0}}), diff --git a/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index 64ba052f1a..e48d387dd1 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -41,7 +41,7 @@ def __call__(self, variable_name: str, period): def test_without_annualize(monthly_variable): - period = periods.build(2019) + period = periods.period.build(2019) person = PopulationMock(monthly_variable) @@ -55,7 +55,7 @@ def test_without_annualize(monthly_variable): def test_with_annualize(monthly_variable): - period = periods.build(2019) + period = periods.period.build(2019) annualized_variable = get_annualized_variable(monthly_variable) person = PopulationMock(annualized_variable) @@ -70,8 +70,8 @@ def test_with_annualize(monthly_variable): def test_with_partial_annualize(monthly_variable): - period = periods.build('year:2018:2') - annualized_variable = get_annualized_variable(monthly_variable, periods.build(2018)) + period = periods.period.build('year:2018:2') + annualized_variable = get_annualized_variable(monthly_variable, periods.period.build(2018)) person = PopulationMock(annualized_variable) From 2fc824e870b58b9fb1ac246a4efdc1de8585eb8a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 18 Dec 2022 17:08:50 +0100 Subject: [PATCH 76/93] Cleanup types --- openfisca_core/types/_domain.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/openfisca_core/types/_domain.py b/openfisca_core/types/_domain.py index 12574b3b9d..6b7ca4bb75 100644 --- a/openfisca_core/types/_domain.py +++ b/openfisca_core/types/_domain.py @@ -69,34 +69,6 @@ def __call__(self, instant: Any) -> ParameterNodeAtInstant: """Abstract method.""" -@typing_extensions.runtime_checkable -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 stop(self) -> Any: - """Abstract method.""" - - @abc.abstractmethod - def offset(self, offset: Any, unit: Any = None) -> Any: - """Abstract method.""" - - class Population(Protocol): """Population protocol.""" From 499d8944a0dca9f72c857eb515f9ce9a30d1f476 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 18 Dec 2022 17:17:35 +0100 Subject: [PATCH 77/93] Add more examples to DateUnit --- openfisca_core/periods/_date_unit.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openfisca_core/periods/_date_unit.py b/openfisca_core/periods/_date_unit.py index d6b6885b5c..8a5f85992d 100644 --- a/openfisca_core/periods/_date_unit.py +++ b/openfisca_core/periods/_date_unit.py @@ -20,9 +20,15 @@ def isoformat(self) -> int: >>> DateUnit.DAY in DateUnit.isoformat True + >>> bool(DateUnit.DAY & DateUnit.isoformat) + True + >>> DateUnit.ETERNITY in DateUnit.isoformat False + >>> bool(DateUnit.ETERNITY & DateUnit.isoformat) + False + .. versionadded:: 39.0.0 """ @@ -61,6 +67,9 @@ class DateUnit(enum.IntFlag, metaclass = DateUnitMeta): >>> DateUnit.DAY in DateUnit True + >>> bool(DateUnit.DAY & ~DateUnit.ETERNITY) + True + >>> "DAY" in DateUnit False From 6addf76a96f501d4bc2f9fa8d8977d0508583e0d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 19 Dec 2022 05:26:00 +0100 Subject: [PATCH 78/93] Remove unused exception --- openfisca_core/periods/_errors.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_errors.py index 81df8797f5..e7a5dde12b 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_errors.py @@ -34,18 +34,6 @@ def __init__(self, value: Any) -> None: ) -class InstantValueError(ValueError): - """Raised when an instant's values are not valid.""" - - def __init__(self, value: Any) -> None: - super().__init__( - f"Invalid instant: '{str(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.""" From f75513855d89d7fe4ef4ef3a0e8491bd335ce979 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 19 Dec 2022 06:05:10 +0100 Subject: [PATCH 79/93] Make simpler test execution --- openfisca_core/periods/__init__.py | 2 +- openfisca_core/periods/{_errors.py => _exceptions.py} | 2 +- openfisca_core/periods/{_date_unit.py => _units.py} | 0 openfisca_core/periods/instant_.py | 6 +++--- openfisca_core/periods/period_.py | 4 ++-- openfisca_tasks/test_code.mk | 5 ----- setup.cfg | 2 +- setup.py | 1 - 8 files changed, 8 insertions(+), 14 deletions(-) rename openfisca_core/periods/{_errors.py => _exceptions.py} (98%) rename openfisca_core/periods/{_date_unit.py => _units.py} (100%) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 76f3add97e..d1dd3abcfe 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -27,8 +27,8 @@ """ -from ._date_unit import DateUnit from ._parsers import ISOFormat +from ._units import DateUnit from .instant_ import Instant from .period_ import Period diff --git a/openfisca_core/periods/_errors.py b/openfisca_core/periods/_exceptions.py similarity index 98% rename from openfisca_core/periods/_errors.py rename to openfisca_core/periods/_exceptions.py index e7a5dde12b..3ef6568507 100644 --- a/openfisca_core/periods/_errors.py +++ b/openfisca_core/periods/_exceptions.py @@ -2,7 +2,7 @@ from typing import Any -from ._date_unit import DateUnit +from ._units import DateUnit day, month, year, _ = tuple(DateUnit) diff --git a/openfisca_core/periods/_date_unit.py b/openfisca_core/periods/_units.py similarity index 100% rename from openfisca_core/periods/_date_unit.py rename to openfisca_core/periods/_units.py diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index c483a6bac6..a6992b6cc7 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -10,14 +10,14 @@ import pendulum from pendulum.datetime import Date -from ._date_unit import DateUnit -from ._errors import ( +from ._exceptions import ( DateUnitValueError, InstantFormatError, InstantTypeError, OffsetTypeError, ) from ._parsers import ISOFormat +from ._units import DateUnit from .typing import Add, Plural DAY, MONTH, YEAR, _ = tuple(DateUnit) @@ -252,7 +252,7 @@ def build(cls, value: Any) -> Instant: >>> Instant.build(period) Traceback (most recent call last): - openfisca_core.periods._errors.InstantFormatError: 'year:2021-09... + InstantFormatError: 'year:2021-09' is not a valid instant. .. versionadded:: 39.0.0 diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 276aaeec43..1405c647a6 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -6,9 +6,9 @@ import inflect -from ._date_unit import DateUnit -from ._errors import DateUnitValueError, PeriodFormatError, PeriodTypeError +from ._exceptions import DateUnitValueError, PeriodFormatError, PeriodTypeError from ._parsers import ISOFormat +from ._units import DateUnit from .instant_ import Instant from .typing import Plural diff --git a/openfisca_tasks/test_code.mk b/openfisca_tasks/test_code.mk index c60c294bf7..6cffe10b17 100644 --- a/openfisca_tasks/test_code.mk +++ b/openfisca_tasks/test_code.mk @@ -31,11 +31,6 @@ test-code: test-core test-country test-extension ## Run openfisca-core tests. test-core: $(shell pytest --quiet --quiet --collect-only 2> /dev/null | cut -f 1 -d ":") @$(call print_help,$@:) - @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 \ ${openfisca} test $? \ diff --git a/setup.cfg b/setup.cfg index 9608c7cfe7..8585166901 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,7 +54,7 @@ skip_empty = true addopts = --doctest-modules --disable-pytest-warnings --showlocals doctest_optionflags = ELLIPSIS IGNORE_EXCEPTION_DETAIL NUMBER NORMALIZE_WHITESPACE python_files = **/*.py -testpaths = openfisca_core/commons/tests openfisca_core/holders/tests openfisca_core/periods/tests tests +testpaths = openfisca_core/commons openfisca_core/holders openfisca_core/periods openfisca_core/types tests [mypy] ignore_missing_imports = True diff --git a/setup.py b/setup.py index 6d7c03ea39..766afdc856 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,6 @@ 'openapi-spec-validator >= 0.3.0', 'pycodestyle >= 2.8.0, < 2.9.0', 'pylint == 2.10.2', - 'xdoctest >= 1.0.0, < 2.0.0', ] + api_requirements setup( From 574973ca92d03bd5e0f9f4dfd2b52437c8406d93 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 19 Dec 2022 08:04:50 +0100 Subject: [PATCH 80/93] Get rid of last type stub --- openfisca_core/periods/_exceptions.py | 7 +--- openfisca_core/periods/_units.py | 29 ++++++++++++++ openfisca_core/periods/_utils.py | 31 ++++++++++++++ openfisca_core/periods/instant_.py | 19 ++++----- openfisca_core/periods/period_.py | 58 ++++++++++++--------------- openfisca_core/periods/typing.py | 17 -------- setup.py | 1 - 7 files changed, 93 insertions(+), 69 deletions(-) create mode 100644 openfisca_core/periods/_utils.py delete mode 100644 openfisca_core/periods/typing.py diff --git a/openfisca_core/periods/_exceptions.py b/openfisca_core/periods/_exceptions.py index 3ef6568507..3f2e127413 100644 --- a/openfisca_core/periods/_exceptions.py +++ b/openfisca_core/periods/_exceptions.py @@ -2,10 +2,6 @@ from typing import Any -from ._units import DateUnit - -day, month, year, _ = tuple(DateUnit) - LEARN_MORE = ( "Learn more about legal period formats in OpenFisca: " "." @@ -18,8 +14,7 @@ class DateUnitValueError(ValueError): def __init__(self, value: Any) -> None: super().__init__( f"'{str(value)}' is not a valid ISO format date unit. ISO format " - f"date units are any of: '{str(day)}', '{str(month)}', or " - f"'{str(year)}'. {LEARN_MORE}" + f"date units are any of: 'day', 'month', or 'year'. {LEARN_MORE}" ) diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_units.py index 8a5f85992d..c5fc27ea38 100644 --- a/openfisca_core/periods/_units.py +++ b/openfisca_core/periods/_units.py @@ -2,6 +2,8 @@ import enum +from ._exceptions import DateUnitValueError + class DateUnitMeta(enum.EnumMeta): """Metaclass for ``DateUnit``.""" @@ -111,3 +113,30 @@ def __str__(self) -> str: except AttributeError: return super().__str__() + + @property + def plural(self) -> str: + """Returns the plural form of the date unit. + + Returns: + str: The plural form. + + Raises: + DateUnitValueError: When the date unit is not a ISO format unit. + + Examples: + >>> DateUnit.DAY.plural + 'days' + + >>> DateUnit.ETERNITY.plural + Traceback (most recent call last): + DateUnitValueError: 'eternity' is not a valid ISO format date unit. + + .. versionadded:: 39.0.0 + + """ + + if self & type(self).isoformat: + return str(self) + "s" + + raise DateUnitValueError(self) diff --git a/openfisca_core/periods/_utils.py b/openfisca_core/periods/_utils.py new file mode 100644 index 0000000000..d9d2206017 --- /dev/null +++ b/openfisca_core/periods/_utils.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import Callable + +from pendulum.datetime import Date + + +def add(date: Date, unit: str, count: int) -> Date: + """Add ``count`` ``unit``s to a ``date``. + + Args: + date: The date to add to. + unit: The unit to add. + count: The number of units to add. + + Returns: + A new Date. + + Examples: + >>> add(Date(2022, 1, 1), "years", 1) + Date(2023, 1, 1) + + .. versionadded:: 39.0.0 + + """ + + fun: Callable[..., Date] = date.add + + new: Date = fun(**{unit: count}) + + return new diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index a6992b6cc7..4bf67d6ef3 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -6,7 +6,6 @@ import datetime import functools -import inflect import pendulum from pendulum.datetime import Date @@ -18,7 +17,7 @@ ) from ._parsers import ISOFormat from ._units import DateUnit -from .typing import Add, Plural +from ._utils import add DAY, MONTH, YEAR, _ = tuple(DateUnit) @@ -77,8 +76,6 @@ class Instant(Tuple[int, int, int]): """ - plural: Plural = inflect.engine().plural - def __repr__(self) -> str: return ( f"{type(self).__name__}" @@ -153,15 +150,15 @@ def date(self) -> Date: return pendulum.date(*self) - def offset(self, offset: str | int, unit: int) -> Instant: + def offset(self, offset: str | int, unit: DateUnit) -> Instant: """Increments/decrements the given instant with offset units. Args: - offset (str | int): How much of ``unit`` to offset. - unit (int): What to offset. + offset: How much of ``unit`` to offset. + unit: What to offset. Returns: - Instant: A new one. + A new Instant. Raises: DateUnitValueError: When ``unit`` is not a date unit. @@ -209,9 +206,7 @@ def offset(self, offset: str | int, unit: int) -> Instant: if not isinstance(offset, int): raise OffsetTypeError(offset) - add: Add = self.date().add - - date = add(**{type(self).plural(str(unit)): offset}) + date = add(self.date(), unit.plural, offset) return type(self)((date.year, date.month, date.day)) @@ -220,7 +215,7 @@ def build(cls, value: Any) -> Instant: """Build a new instant, aka a triple of integers (year, month, day). Args: - value (Any): An ``instant-like`` object. + value: An ``instant-like`` object. Returns: An Instant. diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 1405c647a6..71c4138670 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -4,13 +4,10 @@ import datetime -import inflect - from ._exceptions import DateUnitValueError, PeriodFormatError, PeriodTypeError from ._parsers import ISOFormat from ._units import DateUnit from .instant_ import Instant -from .typing import Plural DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) @@ -21,15 +18,12 @@ class Period(Tuple[DateUnit, Instant, int]): 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``. + unit: Either ``year``, ``month``, ``day`` or ``eternity``. + start: The "instant" the Period starts at. + size: The amount of ``unit``, starting at ``start``, at least ``1``. Args: - tuple(str, .Instant, int))): + (tuple(DateUnit, .Instant, int)): The ``unit``, ``start``, and ``size``, accordingly. Examples: @@ -78,8 +72,6 @@ class Period(Tuple[DateUnit, Instant, int]): """ - plural: Plural = inflect.engine().plural - def __repr__(self) -> str: return ( f"{type(self).__name__}" @@ -90,7 +82,7 @@ def __str__(self) -> str: """Transform period to a string. Returns: - str: A string representation of the period. + A string representation of the period. Examples: >>> jan = Instant((2021, 1, 1)) @@ -155,7 +147,7 @@ def __contains__(self, other: object) -> bool: """Checks if a ``period`` contains another one. Args: - other (object): The other ``Period``. + other: The other ``Period``. Returns: True if ``other`` is contained, otherwise False. @@ -175,11 +167,11 @@ def __contains__(self, other: object) -> bool: return super().__contains__(other) @property - def unit(self) -> int: + def unit(self) -> DateUnit: """The ``unit`` of the ``Period``. Returns: - An int. + A DateUnit. Example: >>> start = Instant((2021, 10, 1)) @@ -284,7 +276,7 @@ def date(self) -> datetime.date: return self.start.date() - def count(self, unit: int) -> int: + def count(self, unit: DateUnit) -> int: """The ``size`` of the ``Period`` in the given unit. Args: @@ -340,15 +332,15 @@ def count(self, unit: int) -> int: return self.size * 12 raise ValueError( - f"Cannot calculate number of {type(self).plural(str(unit))} in a " + f"Cannot calculate number of {unit.plural} in a " f"{str(self.unit)}." ) - def this(self, unit: int) -> Period: + def this(self, unit: DateUnit) -> Period: """A new month ``Period`` starting at the first of ``unit``. Args: - unit (int): The unit of the requested Period. + unit: The unit of the requested Period. Returns: A Period. @@ -373,12 +365,12 @@ def this(self, unit: int) -> Period: return type(self)((unit, self.start.offset("first-of", unit), 1)) - def last(self, unit: int, size: int = 1) -> Period: + def last(self, unit: DateUnit, size: int = 1) -> Period: """Last ``size`` ``unit``s of the ``Period``. Args: - unit (int): The unit of the requested Period. - size (int): The number of units to include in the Period. + unit: The unit of the requested Period. + size: The number of units to include in the Period. Returns: A Period. @@ -412,12 +404,12 @@ def last(self, unit: int, size: int = 1) -> Period: return type(self)((unit, self.ago(unit, size).start, size)) - def ago(self, unit: int, size: int = 1) -> Period: + def ago(self, unit: DateUnit, size: int = 1) -> Period: """``size`` ``unit``s ago of the ``Period``. Args: - unit (int): The unit of the requested Period. - size (int): The number of units ago. + unit: The unit of the requested Period. + size: The number of units ago. Returns: A Period. @@ -451,12 +443,12 @@ def ago(self, unit: int, size: int = 1) -> Period: return type(self)((unit, self.this(unit).start, 1)).offset(-size) - def offset(self, offset: str | int, unit: int | None = None) -> Period: + def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: """Increment (or decrement) the given period with offset units. Args: - offset (str | int): How much of ``unit`` to offset. - unit (int, optional): What to offset. + offset: How much of ``unit`` to offset. + unit: What to offset. Returns: Period: A new one. @@ -487,11 +479,11 @@ def offset(self, offset: str | int, unit: int | None = None) -> Period: return type(self)((self.unit, start, self.size)) - def subperiods(self, unit: int) -> Sequence[Period]: + def subperiods(self, unit: DateUnit) -> Sequence[Period]: """Return the list of all the periods of unit ``unit``. Args: - unit (int): A string representing period's ``unit``. + unit: A string representing period's ``unit``. Returns: A list of periods. @@ -532,7 +524,7 @@ def build(cls, value: Any) -> Period: """Build a new period, aka a triple (unit, start_instant, size). Args: - value (Any): A ``period-like`` object. + value: A ``period-like`` object. Returns: A period. @@ -574,7 +566,7 @@ def build(cls, value: Any) -> Period: """ - unit: int | str + unit: DateUnit | int | str part: ISOFormat | None size: int | str diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py deleted file mode 100644 index 3274d2dd00..0000000000 --- a/openfisca_core/periods/typing.py +++ /dev/null @@ -1,17 +0,0 @@ -# pylint: disable=missing-class-docstring,missing-function-docstring - -from __future__ import annotations - -from typing_extensions import Protocol - -from pendulum.datetime import Date - - -class Add(Protocol): - def __call__(self, years: int, months: int, week: int, days: int) -> Date: - ... - - -class Plural(Protocol): - def __call__(self, text: str, count: str | int | None = None) -> str: - ... diff --git a/setup.py b/setup.py index 766afdc856..85b349713b 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,6 @@ 'PyYAML >= 3.10', 'dpath >= 1.5.0, < 3.0.0', 'importlib-metadata < 4.3.0', - 'inflect >= 6.0.0, < 7.0.0', 'nptyping == 1.4.4', 'numexpr >= 2.7.0, <= 3.0', 'numpy >= 1.20, < 1.21', From a6c00c9ec0f4fbb769dd176754da21c47c22b993 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 19 Dec 2022 10:56:04 +0100 Subject: [PATCH 81/93] Minimise changes to the public API --- CHANGELOG.md | 2 - .../data_storage/in_memory_storage.py | 12 +- .../data_storage/on_disk_storage.py | 14 +- openfisca_core/holders/helpers.py | 6 +- openfisca_core/holders/holder.py | 4 +- openfisca_core/holders/tests/test_helpers.py | 8 +- openfisca_core/parameters/at_instant_like.py | 2 +- openfisca_core/parameters/helpers.py | 2 +- openfisca_core/parameters/parameter.py | 4 +- openfisca_core/periods/__init__.py | 21 +- openfisca_core/periods/_builders.py | 214 +++++++++++++++ .../periods/{_units.py => _date_unit.py} | 0 .../periods/{instant_.py => _instant.py} | 81 +----- openfisca_core/periods/_iso_format.py | 22 ++ openfisca_core/periods/_parsers.py | 243 ++++++++---------- .../periods/{period_.py => _period.py} | 139 +--------- openfisca_core/periods/tests/test_builders.py | 109 ++++++++ openfisca_core/periods/tests/test_instant.py | 48 +--- .../{test__parsers.py => test_parsers.py} | 16 +- openfisca_core/periods/tests/test_period.py | 59 ----- openfisca_core/populations/population.py | 10 +- .../scripts/measure_performances.py | 2 +- openfisca_core/simulations/simulation.py | 22 +- .../simulations/simulation_builder.py | 8 +- .../taxbenefitsystems/tax_benefit_system.py | 12 +- openfisca_core/tracers/full_tracer.py | 8 +- openfisca_core/tracers/simple_tracer.py | 4 +- openfisca_core/tracers/trace_node.py | 2 +- openfisca_core/variables/helpers.py | 2 +- openfisca_core/variables/variable.py | 8 +- tests/core/test_countries.py | 2 +- tests/core/test_cycles.py | 2 +- tests/core/test_holders.py | 30 +-- tests/core/test_opt_out_cache.py | 2 +- tests/core/test_reforms.py | 34 +-- tests/core/variables/test_annualize.py | 8 +- 36 files changed, 587 insertions(+), 575 deletions(-) create mode 100644 openfisca_core/periods/_builders.py rename openfisca_core/periods/{_units.py => _date_unit.py} (100%) rename openfisca_core/periods/{instant_.py => _instant.py} (69%) create mode 100644 openfisca_core/periods/_iso_format.py rename openfisca_core/periods/{period_.py => _period.py} (77%) create mode 100644 openfisca_core/periods/tests/test_builders.py rename openfisca_core/periods/tests/{test__parsers.py => test_parsers.py} (91%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f5b64ce2b..d2f74a4b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,6 @@ ##### Renames - Rename `periods.period.get_subperiods` to `periods.period.subperiods`. -- Rename `periods.instant` to `periods.instant.build`. -- Rename `periods.period` to `periods.period.build`. ##### Deprecations diff --git a/openfisca_core/data_storage/in_memory_storage.py b/openfisca_core/data_storage/in_memory_storage.py index 8d391d5dcf..d04d3ec437 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.build(periods.ETERNITY) - period = periods.period.build(period) + period = periods.period(periods.ETERNITY) + period = periods.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.build(periods.ETERNITY) - period = periods.period.build(period) + period = periods.period(periods.ETERNITY) + period = periods.period(period) self._arrays[period] = value @@ -35,8 +35,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.period.build(periods.ETERNITY) - period = periods.period.build(period) + period = periods.period(periods.ETERNITY) + period = periods.period(period) self._arrays = { period_item: value diff --git a/openfisca_core/data_storage/on_disk_storage.py b/openfisca_core/data_storage/on_disk_storage.py index 04dd45d720..ae886c492f 100644 --- a/openfisca_core/data_storage/on_disk_storage.py +++ b/openfisca_core/data_storage/on_disk_storage.py @@ -28,8 +28,8 @@ def _decode_file(self, file): def get(self, period): if self.is_eternal: - period = periods.period.build(periods.ETERNITY) - period = periods.period.build(period) + period = periods.period(periods.ETERNITY) + period = periods.period(period) values = self._files.get(period) if values is None: @@ -38,8 +38,8 @@ def get(self, period): def put(self, value, period): if self.is_eternal: - period = periods.period.build(periods.ETERNITY) - period = periods.period.build(period) + period = periods.period(periods.ETERNITY) + period = periods.period(period) filename = str(period) path = os.path.join(self.storage_dir, filename) + '.npy' @@ -55,8 +55,8 @@ def delete(self, period = None): return if self.is_eternal: - period = periods.period.build(periods.ETERNITY) - period = periods.period.build(period) + period = periods.period(periods.ETERNITY) + period = periods.period(period) if period is not None: self._files = { @@ -76,7 +76,7 @@ def restore(self): continue path = os.path.join(self.storage_dir, filename) filename_core = filename.rsplit('.', 1)[0] - period = periods.period.build(filename_core) + period = periods.period(filename_core) files[period] = path def __del__(self): diff --git a/openfisca_core/holders/helpers.py b/openfisca_core/holders/helpers.py index e9cd9b06b3..813426d805 100644 --- a/openfisca_core/holders/helpers.py +++ b/openfisca_core/holders/helpers.py @@ -27,7 +27,7 @@ def set_input_dispatch_by_period(holder, period, array): after_instant = period.start.offset(period_size, period_unit) # Cache the input data, skipping the existing cached months - sub_period = periods.period((cached_period_unit, period.start, 1)) + sub_period = periods.Period((cached_period_unit, period.start, 1)) while sub_period.start < after_instant: existing_array = holder.get_array(sub_period) if existing_array is None: @@ -60,7 +60,7 @@ def set_input_divide_by_period(holder, period, array): # Count the number of elementary periods to change, and the difference with what is already known. remaining_array = array.copy() - sub_period = periods.period((cached_period_unit, period.start, 1)) + sub_period = periods.Period((cached_period_unit, period.start, 1)) sub_periods_count = 0 while sub_period.start < after_instant: existing_array = holder.get_array(sub_period) @@ -73,7 +73,7 @@ def set_input_divide_by_period(holder, period, array): # Cache the input data if sub_periods_count > 0: divided_array = remaining_array / sub_periods_count - sub_period = periods.period((cached_period_unit, period.start, 1)) + sub_period = periods.Period((cached_period_unit, period.start, 1)) while sub_period.start < after_instant: if holder.get_array(sub_period) is None: holder._set(sub_period, divided_array) diff --git a/openfisca_core/holders/holder.py b/openfisca_core/holders/holder.py index 368b234add..d74e81910f 100644 --- a/openfisca_core/holders/holder.py +++ b/openfisca_core/holders/holder.py @@ -160,7 +160,7 @@ def get_known_periods(self): def set_input( self, - period: periods.period, + period: periods.Period, array: numpy.ndarray | Sequence[Any], ) -> numpy.ndarray | None: """Set a Variable's array of values of a given Period. @@ -206,7 +206,7 @@ def set_input( """ - period = periods.period.build(period) + period = periods.period(period) if period is None: raise ValueError(f"Invalid period value: {period}") diff --git a/openfisca_core/holders/tests/test_helpers.py b/openfisca_core/holders/tests/test_helpers.py index 6574c2f008..333e319a61 100644 --- a/openfisca_core/holders/tests/test_helpers.py +++ b/openfisca_core/holders/tests/test_helpers.py @@ -57,8 +57,8 @@ def test_set_input_dispatch_by_period( Income.definition_period = definition_unit income = Income() holder = Holder(income, population) - instant = periods.instant((2022, 1, 1)) - dispatch_period = periods.period((dispatch_unit, instant, 3)) + instant = periods.Instant((2022, 1, 1)) + dispatch_period = periods.Period((dispatch_unit, instant, 3)) holders.set_input_dispatch_by_period(holder, dispatch_period, values) total = sum(map(holder.get_array, holder.get_known_periods())) @@ -88,8 +88,8 @@ def test_set_input_divide_by_period( Income.definition_period = definition_unit income = Income() holder = Holder(income, population) - instant = periods.instant((2022, 1, 1)) - divide_period = periods.period((divide_unit, instant, 3)) + instant = periods.Instant((2022, 1, 1)) + divide_period = periods.Period((divide_unit, instant, 3)) holders.set_input_divide_by_period(holder, divide_period, values) last = holder.get_array(holder.get_known_periods()[-1]) diff --git a/openfisca_core/parameters/at_instant_like.py b/openfisca_core/parameters/at_instant_like.py index 965348b77f..1a1db34beb 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.build(instant)) + instant = str(periods.instant(instant)) return self._get_at_instant(instant) @abc.abstractmethod diff --git a/openfisca_core/parameters/helpers.py b/openfisca_core/parameters/helpers.py index 9426de638c..1c24fcb48f 100644 --- a/openfisca_core/parameters/helpers.py +++ b/openfisca_core/parameters/helpers.py @@ -63,7 +63,7 @@ def _parse_child(child_name, child, child_path): return parameters.Parameter(child_name, child, child_path) elif 'brackets' in child: return parameters.ParameterScale(child_name, child, child_path) - elif isinstance(child, dict) and all([periods.isoformat.fromstr(str(key)) for key in child.keys()]): + elif isinstance(child, dict) and all([periods.parse(str(key)) for key in child.keys()]): return parameters.Parameter(child_name, child, child_path) else: return parameters.ParameterNode(child_name, data = child, file_path = child_path) diff --git a/openfisca_core/parameters/parameter.py b/openfisca_core/parameters/parameter.py index 25bd7ce22e..66ffbf1dc7 100644 --- a/openfisca_core/parameters/parameter.py +++ b/openfisca_core/parameters/parameter.py @@ -73,7 +73,7 @@ def __init__(self, name: str, data: dict, file_path: Optional[str] = None) -> No values_list = [] for instant_str in instants: - if periods.isoformat.fromstr(instant_str) is None: + if periods.parse(instant_str) is None: raise ParameterParsingError( "Invalid property '{}' in '{}'. Properties must be valid YYYY-MM-DD instants, such as 2017-01-15." .format(instant_str, self.name), @@ -120,7 +120,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.build(period) + period = periods.period(period) start = period.start stop = period.stop if start is None: diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index d1dd3abcfe..fcf29f3657 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -27,19 +27,14 @@ """ -from ._parsers import ISOFormat -from ._units import DateUnit -from .instant_ import Instant -from .period_ import Period - -DAY = DateUnit.DAY -MONTH = DateUnit.MONTH -YEAR = DateUnit.YEAR -ETERNITY = DateUnit.ETERNITY -dateunit = DateUnit -instant = Instant -period = Period -isoformat = ISOFormat +from . import _parsers as isoformat +from ._builders import instant, period +from ._date_unit import DateUnit +from ._instant import Instant +from ._parsers import fromstr as parse +from ._period import Period + +DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) # Deprecated diff --git a/openfisca_core/periods/_builders.py b/openfisca_core/periods/_builders.py new file mode 100644 index 0000000000..241cea0912 --- /dev/null +++ b/openfisca_core/periods/_builders.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +from typing import Any + +import datetime + +from ._date_unit import DateUnit +from ._exceptions import ( + InstantFormatError, + InstantTypeError, + PeriodFormatError, + PeriodTypeError, + ) +from ._instant import Instant +from ._iso_format import ISOFormat +from ._parsers import fromint, fromseq, fromstr +from ._period import Period + +day, _month, year, eternity = tuple(DateUnit) + + +def instant(value: Any) -> Instant: + """Build a new instant, aka a triple of integers (year, month, day). + + Args: + value: An ``instant-like`` object. + + Returns: + An Instant. + + Raises: + InstantFormatError: When ``value`` is invalid, like "2021-32-13". + InstantTypeError: When ``value`` is None. + + Examples: + >>> instant(datetime.date(2021, 9, 16)) + Instant((2021, 9, 16)) + + >>> instant(Instant((2021, 9, 16))) + Instant((2021, 9, 16)) + + >>> instant("2021") + Instant((2021, 1, 1)) + + >>> instant(2021) + Instant((2021, 1, 1)) + + >>> instant((2021, 9)) + Instant((2021, 9, 1)) + + >>> start = Instant((2021, 9, 16)) + + >>> instant(Period((year, start, 1))) + Traceback (most recent call last): + InstantFormatError: 'year:2021-09' is not a valid instant. + + .. versionadded:: 39.0.0 + + """ + + isoformat: ISOFormat | None + + if isinstance(value, Instant): + return value + + if isinstance(value, datetime.date): + return Instant((value.year, value.month, value.day)) + + if isinstance(value, int): + isoformat = fromint(value) + + elif isinstance(value, str): + isoformat = fromstr(value) + + elif isinstance(value, (list, tuple)): + isoformat = fromseq(value) + + else: + raise InstantTypeError(value) + + if isoformat is None: + raise InstantFormatError(value) + + return Instant((isoformat.year, isoformat.month, isoformat.day)) + + +def period(value: Any) -> Period: + """Build a new period, aka a triple (unit, start_instant, size). + + Args: + value: A ``period-like`` object. + + Returns: + A period. + + Raises: + PeriodFormatError: When arguments are invalid, like "2021-32-13". + PeriodTypeError: When ``value`` is not a ``period-like`` object. + + Examples: + >>> period(Period((year, Instant((2021, 1, 1)), 1))) + Period((year, Instant((2021, 1, 1)), 1)) + + >>> period(Instant((2021, 1, 1))) + Period((day, Instant((2021, 1, 1)), 1)) + + >>> period(eternity) + Period((eternity, Instant((1, 1, 1)), 1)) + + >>> period(2021) + Period((year, Instant((2021, 1, 1)), 1)) + + >>> period("2014") + Period((year, Instant((2014, 1, 1)), 1)) + + >>> period("year:2014") + Period((year, Instant((2014, 1, 1)), 1)) + + >>> period("month:2014-02") + Period((month, Instant((2014, 2, 1)), 1)) + + >>> period("year:2014-02") + Period((year, Instant((2014, 2, 1)), 1)) + + >>> period("day:2014-02-02") + Period((day, Instant((2014, 2, 2)), 1)) + + >>> period("day:2014-02-02:3") + Period((day, Instant((2014, 2, 2)), 3)) + + """ + + unit: DateUnit | int | str + part: ISOFormat | None + size: int | str + + if value in {eternity, eternity.name, eternity.name.lower()}: + return Period((eternity, instant(datetime.date.min), 1)) + + if value is None or isinstance(value, DateUnit): + raise PeriodTypeError(value) + + if isinstance(value, Period): + return value + + if isinstance(value, Instant): + return Period((day, value, 1)) + + if isinstance(value, int): + return Period((year, Instant((value, 1, 1)), 1)) + + if not isinstance(value, str): + raise PeriodFormatError(value) + + # Try to parse as a simple period + part = fromstr(value) + + if part is not None: + start = Instant((part.year, part.month, part.day)) + return Period((DateUnit(part.unit), start, 1)) + + # Complex periods must have a ':' in their strings + if ":" not in value: + raise PeriodFormatError(value) + + # We know the first element has to be a ``unit`` + unit, *rest = value.split(":") + + # Units are case insensitive so we need to upper them + unit = unit.upper() + + # Left-most component must be a valid unit + if unit not in dir(DateUnit): + raise PeriodFormatError(value) + + unit = DateUnit[unit] + + # We get the first remaining component + date, *rest = rest + + if date is None: + raise PeriodFormatError(value) + + # Middle component must be a valid ISO period + part = fromstr(date) + + if part is None: + raise PeriodFormatError(value) + + # Finally we try to parse the size, if any + try: + size, *rest = rest + + except ValueError: + size = 1 + + # If provided, make sure the size is an integer + try: + size = int(size) + + except ValueError: + raise PeriodFormatError(value) + + # If there were more than 2 ":" in the string, the period is invalid + if len(rest) > 0: + raise PeriodFormatError(value) + + # Reject ambiguous periods such as month:2014 + if part.unit > unit: + raise PeriodFormatError(value) + + start = Instant((part.year, part.month, part.day)) + + return Period((unit, start, size)) diff --git a/openfisca_core/periods/_units.py b/openfisca_core/periods/_date_unit.py similarity index 100% rename from openfisca_core/periods/_units.py rename to openfisca_core/periods/_date_unit.py diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/_instant.py similarity index 69% rename from openfisca_core/periods/instant_.py rename to openfisca_core/periods/_instant.py index 4bf67d6ef3..bb4d840b7f 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/_instant.py @@ -1,22 +1,15 @@ from __future__ import annotations -from typing import Any, Tuple +from typing import Tuple import calendar -import datetime import functools import pendulum from pendulum.datetime import Date -from ._exceptions import ( - DateUnitValueError, - InstantFormatError, - InstantTypeError, - OffsetTypeError, - ) -from ._parsers import ISOFormat -from ._units import DateUnit +from ._date_unit import DateUnit +from ._exceptions import DateUnitValueError, OffsetTypeError from ._utils import add DAY, MONTH, YEAR, _ = tuple(DateUnit) @@ -209,71 +202,3 @@ def offset(self, offset: str | int, unit: DateUnit) -> Instant: date = add(self.date(), unit.plural, offset) return type(self)((date.year, date.month, date.day)) - - @classmethod - def build(cls, value: Any) -> Instant: - """Build a new instant, aka a triple of integers (year, month, day). - - Args: - value: An ``instant-like`` object. - - Returns: - An Instant. - - Raises: - InstantFormatError: When ``value`` is invalid, like "2021-32-13". - InstantTypeError: When ``value`` is None. - - Examples: - >>> from openfisca_core import periods - - >>> Instant.build(datetime.date(2021, 9, 16)) - Instant((2021, 9, 16)) - - >>> Instant.build(Instant((2021, 9, 16))) - Instant((2021, 9, 16)) - - >>> Instant.build("2021") - Instant((2021, 1, 1)) - - >>> Instant.build(2021) - Instant((2021, 1, 1)) - - >>> Instant.build((2021, 9)) - Instant((2021, 9, 1)) - - >>> start = Instant((2021, 9, 16)) - >>> period = periods.period((YEAR, start, 1)) - - >>> Instant.build(period) - Traceback (most recent call last): - InstantFormatError: 'year:2021-09' is not a valid instant. - - .. versionadded:: 39.0.0 - - """ - - instant: ISOFormat | None - - if isinstance(value, Instant): - return value - - if isinstance(value, datetime.date): - return cls((value.year, value.month, value.day)) - - if isinstance(value, int): - instant = ISOFormat.fromint(value) - - elif isinstance(value, str): - instant = ISOFormat.fromstr(value) - - elif isinstance(value, (list, tuple)): - instant = ISOFormat.fromseq(value) - - else: - raise InstantTypeError(value) - - if instant is None: - raise InstantFormatError(value) - - return cls((instant.year, instant.month, instant.day)) diff --git a/openfisca_core/periods/_iso_format.py b/openfisca_core/periods/_iso_format.py new file mode 100644 index 0000000000..0fb7ab834b --- /dev/null +++ b/openfisca_core/periods/_iso_format.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from typing import NamedTuple + + +class ISOFormat(NamedTuple): + """An implementation of the `parse` protocol.""" + + #: The year of the parsed period. + year: int + + #: The month of the parsed period. + month: int + + #: The month of the parsed period. + day: int + + #: The unit of the parsed period, in binary. + unit: int + + #: The number of fragments in the parsed period. + shape: int diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index 4f89c1bde1..454715c560 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -1,199 +1,182 @@ from __future__ import annotations -from typing import NamedTuple, Sequence +from typing import Sequence import pendulum from pendulum.datetime import Date from pendulum.parsing import ParserError +from ._iso_format import ISOFormat -class ISOFormat(NamedTuple): - """An implementation of the `parse` protocol.""" - #: The year of the parsed period. - year: int +def fromint(value: int) -> ISOFormat | None: + """Parse an int respecting the ISO format. - #: The month of the parsed period. - month: int + Args: + value: The integer to parse. - #: The month of the parsed period. - day: int + Returns: + An ISOFormat object if ``value`` is valid. + None otherwise. - #: The unit of the parsed period, in binary. - unit: int + Examples: + >>> fromint(1) + ISOFormat(year=1, month=1, day=1, unit=4, shape=1) - #: The number of fragments in the parsed period. - shape: int + >>> fromint(2023) + ISOFormat(year=2023, month=1, day=1, unit=4, shape=1) - @classmethod - def fromint(cls, value: int) -> ISOFormat | None: - """Parse an int respecting the ISO format. + >>> fromint(-1) - Args: - value: The integer to parse. + >>> fromint("2023") - Returns: - An ISOFormat object if ``value`` is valid. - None otherwise. + >>> fromint(20231231) - Examples: - >>> ISOFormat.fromint(1) - ISOFormat(year=1, month=1, day=1, unit=4, shape=1) + .. versionadded:: 39.0.0 - >>> ISOFormat.fromint(2023) - ISOFormat(year=2023, month=1, day=1, unit=4, shape=1) + """ - >>> ISOFormat.fromint(-1) + if not isinstance(value, int): + return None - >>> ISOFormat.fromint("2023") + if not 1 <= len(str(value)) <= 4: + return None - >>> ISOFormat.fromint(20231231) - - .. versionadded:: 39.0.0 - - """ - - if not isinstance(value, int): + try: + if not 1 <= int(str(value)[:4]) < 10000: return None - if not 1 <= len(str(value)) <= 4: - return None + except ValueError: + return None - try: - if not 1 <= int(str(value)[:4]) < 10000: - return None + return ISOFormat(value, 1, 1, 4, 1) - except ValueError: - return None - return cls(value, 1, 1, 4, 1) +def fromstr(value: str) -> ISOFormat | None: + """Parse strings respecting the ISO format. - @classmethod - def fromstr(cls, value: str) -> ISOFormat | None: - """Parse strings respecting the ISO format. + Args: + value: A string such as such as "2012" or "2015-03". - Args: - value: A string such as such as "2012" or "2015-03". + Returns: + An ISOFormat object if ``value`` is valid. + None if ``value`` is not valid. - Returns: - An ISOFormat object if ``value`` is valid. - None if ``value`` is not valid. + Examples: + >>> fromstr("2022") + ISOFormat(year=2022, month=1, day=1, unit=4, shape=1) - Examples: - >>> ISOFormat.fromstr("2022") - ISOFormat(year=2022, month=1, day=1, unit=4, shape=1) + >>> fromstr("2022-02") + ISOFormat(year=2022, month=2, day=1, unit=2, shape=2) - >>> ISOFormat.fromstr("2022-02") - ISOFormat(year=2022, month=2, day=1, unit=2, shape=2) + >>> fromstr("2022-02-13") + ISOFormat(year=2022, month=2, day=13, unit=1, shape=3) - >>> ISOFormat.fromstr("2022-02-13") - ISOFormat(year=2022, month=2, day=13, unit=1, shape=3) + >>> fromstr(1000) - >>> ISOFormat.fromstr(1000) + >>> fromstr("ETERNITY") - >>> ISOFormat.fromstr("ETERNITY") + .. versionadded:: 39.0.0 - .. versionadded:: 39.0.0 + """ - """ + if not isinstance(value, str): + return None - if not isinstance(value, str): - return None + if not value: + return None - if not value: - return None + # If it is a complex value, next! + if len(value.split(":")) != 1: + return None - # If it is a complex value, next! - if len(value.split(":")) != 1: - return None + # If it's negative period, next! + if value[0] == "-": + return None - # If it's negative period, next! - if value[0] == "-": - return None + try: + # We parse the date + date = pendulum.parse(value, exact = True, strict = True) - try: - # We parse the date - date = pendulum.parse(value, exact = True, strict = True) + except ParserError: + return None - except ParserError: - return None + if not isinstance(date, Date): + return None - if not isinstance(date, Date): - return None + # We get the shape of the string (e.g. "2012-02" = 2) + shape = len(value.split("-")) - # We get the shape of the string (e.g. "2012-02" = 2) - shape = len(value.split("-")) + # We get the unit from the shape (e.g. 2 = "month") + unit = pow(2, 3 - shape) - # We get the unit from the shape (e.g. 2 = "month") - unit = pow(2, 3 - shape) + # We build the corresponding ISOFormat object + return ISOFormat(date.year, date.month, date.day, unit, shape) - # We build the corresponding ISOFormat object - return cls(date.year, date.month, date.day, unit, shape) - @classmethod - def fromseq(cls, value: Sequence[int]) -> ISOFormat | None: - """Parse a sequence of ints respecting the ISO format. +def fromseq(value: Sequence[int]) -> ISOFormat | None: + """Parse a sequence of ints respecting the ISO format. - Args: - value: A sequence of ints such as [2012, 3, 13]. + Args: + value: A sequence of ints such as [2012, 3, 13]. - Returns: - An ISOFormat object if ``value`` is valid. - None if ``value`` is not valid. + Returns: + An ISOFormat object if ``value`` is valid. + None if ``value`` is not valid. - Examples: - >>> ISOFormat.fromseq([2022]) - ISOFormat(year=2022, month=1, day=1, unit=4, shape=1) + Examples: + >>> fromseq([2022]) + ISOFormat(year=2022, month=1, day=1, unit=4, shape=1) - >>> ISOFormat.fromseq([2022, 1]) - ISOFormat(year=2022, month=1, day=1, unit=2, shape=2) + >>> fromseq([2022, 1]) + ISOFormat(year=2022, month=1, day=1, unit=2, shape=2) - >>> ISOFormat.fromseq([2022, 1, 1]) - ISOFormat(year=2022, month=1, day=1, unit=1, shape=3) + >>> fromseq([2022, 1, 1]) + ISOFormat(year=2022, month=1, day=1, unit=1, shape=3) - >>> ISOFormat.fromseq([-2022, 1, 1]) + >>> fromseq([-2022, 1, 1]) - >>> ISOFormat.fromseq([2022, 13, 1]) + >>> fromseq([2022, 13, 1]) - >>> ISOFormat.fromseq([2022, 1, 32]) + >>> fromseq([2022, 1, 32]) - .. versionadded:: 39.0.0 + .. versionadded:: 39.0.0 - """ + """ - if not isinstance(value, (list, tuple)): - return None + if not isinstance(value, (list, tuple)): + return None - if not value: - return None + if not value: + return None - if not 1 <= len(value) <= 3: - return None + if not 1 <= len(value) <= 3: + return None - if not all(isinstance(unit, int) for unit in value): - return None + if not all(isinstance(unit, int) for unit in value): + return None - if not all(unit == abs(unit) for unit in value): - return None + if not all(unit == abs(unit) for unit in value): + return None - # We get the shape of the string (e.g. "2012-02" = 2) - shape = len(value) + # We get the shape of the string (e.g. "2012-02" = 2) + shape = len(value) - # We get the unit from the shape (e.g. 2 = "month") - unit = pow(2, 3 - shape) + # We get the unit from the shape (e.g. 2 = "month") + unit = pow(2, 3 - shape) - while len(value) < 3: - value = (*value, 1) + while len(value) < 3: + value = (*value, 1) - try: - # We parse the date - date = pendulum.date(*value) + try: + # We parse the date + date = pendulum.date(*value) - except ValueError: - return None + except ValueError: + return None - if not isinstance(date, Date): - return None + if not isinstance(date, Date): + return None - # We build the corresponding ISOFormat object - return cls(date.year, date.month, date.day, unit, shape) + # We build the corresponding ISOFormat object + return ISOFormat(date.year, date.month, date.day, unit, shape) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/_period.py similarity index 77% rename from openfisca_core/periods/period_.py rename to openfisca_core/periods/_period.py index 71c4138670..3605ec4fb9 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/_period.py @@ -1,13 +1,12 @@ from __future__ import annotations -from typing import Any, Sequence, Tuple +from typing import Sequence, Tuple import datetime -from ._exceptions import DateUnitValueError, PeriodFormatError, PeriodTypeError -from ._parsers import ISOFormat -from ._units import DateUnit -from .instant_ import Instant +from ._date_unit import DateUnit +from ._exceptions import DateUnitValueError +from ._instant import Instant DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) @@ -518,133 +517,3 @@ def subperiods(self, unit: DateUnit) -> Sequence[Period]: self.this(unit).offset(offset, unit) for offset in range(self.count(unit)) ] - - @classmethod - def build(cls, value: Any) -> Period: - """Build a new period, aka a triple (unit, start_instant, size). - - Args: - value: A ``period-like`` object. - - Returns: - A period. - - Raises: - PeriodFormatError: When arguments are invalid, like "2021-32-13". - PeriodTypeError: When ``value`` is not a ``period-like`` object. - - Examples: - >>> Period.build(Period((YEAR, Instant((2021, 1, 1)), 1))) - Period((year, Instant((2021, 1, 1)), 1)) - - >>> Period.build(Instant((2021, 1, 1))) - Period((day, Instant((2021, 1, 1)), 1)) - - >>> Period.build(ETERNITY) - Period((eternity, Instant((1, 1, 1)), 1)) - - >>> Period.build(2021) - Period((year, Instant((2021, 1, 1)), 1)) - - >>> Period.build("2014") - Period((year, Instant((2014, 1, 1)), 1)) - - >>> Period.build("year:2014") - Period((year, Instant((2014, 1, 1)), 1)) - - >>> Period.build("month:2014-02") - Period((month, Instant((2014, 2, 1)), 1)) - - >>> Period.build("year:2014-02") - Period((year, Instant((2014, 2, 1)), 1)) - - >>> Period.build("day:2014-02-02") - Period((day, Instant((2014, 2, 2)), 1)) - - >>> Period.build("day:2014-02-02:3") - Period((day, Instant((2014, 2, 2)), 3)) - - """ - - unit: DateUnit | int | str - part: ISOFormat | None - size: int | str - - if value in {ETERNITY, ETERNITY.name, ETERNITY.name.lower()}: - return cls((ETERNITY, Instant.build(datetime.date.min), 1)) - - if value is None or isinstance(value, DateUnit): - raise PeriodTypeError(value) - - if isinstance(value, Period): - return value - - if isinstance(value, Instant): - return cls((DAY, value, 1)) - - if isinstance(value, int): - return cls((YEAR, Instant((value, 1, 1)), 1)) - - if not isinstance(value, str): - raise PeriodFormatError(value) - - # Try to parse as a simple period - part = ISOFormat.fromstr(value) - - if part is not None: - start = Instant((part.year, part.month, part.day)) - return cls((DateUnit(part.unit), start, 1)) - - # Complex periods must have a ':' in their strings - if ":" not in value: - raise PeriodFormatError(value) - - # We know the first element has to be a ``unit`` - unit, *rest = value.split(":") - - # Units are case insensitive so we need to upper them - unit = unit.upper() - - # Left-most component must be a valid unit - if unit not in dir(DateUnit): - raise PeriodFormatError(value) - - unit = DateUnit[unit] - - # We get the first remaining component - date, *rest = rest - - if date is None: - raise PeriodFormatError(value) - - # Middle component must be a valid ISO period - part = ISOFormat.fromstr(date) - - if part is None: - raise PeriodFormatError(value) - - # Finally we try to parse the size, if any - try: - size, *rest = rest - - except ValueError: - size = 1 - - # If provided, make sure the size is an integer - try: - size = int(size) - - except ValueError: - raise PeriodFormatError(value) - - # If there were more than 2 ":" in the string, the period is invalid - if len(rest) > 0: - raise PeriodFormatError(value) - - # Reject ambiguous periods such as month:2014 - if part.unit > unit: - raise PeriodFormatError(value) - - start = Instant((part.year, part.month, part.day)) - - return cls((unit, start, size)) diff --git a/openfisca_core/periods/tests/test_builders.py b/openfisca_core/periods/tests/test_builders.py new file mode 100644 index 0000000000..9227d92003 --- /dev/null +++ b/openfisca_core/periods/tests/test_builders.py @@ -0,0 +1,109 @@ +import datetime + +import pytest + +from openfisca_core import periods +from openfisca_core.periods import DateUnit, Instant, Period + +day, month, year, eternity = DateUnit + + +@pytest.mark.parametrize("arg, expected", [ + ["1000", Instant((1000, 1, 1))], + ["1000-01", Instant((1000, 1, 1))], + ["1000-01-01", Instant((1000, 1, 1))], + [1000, Instant((1000, 1, 1))], + [(1000,), Instant((1000, 1, 1))], + [(1000, 1), Instant((1000, 1, 1))], + [(1000, 1, 1), Instant((1000, 1, 1))], + [datetime.date(1, 1, 1), Instant((1, 1, 1))], + [Instant((1, 1, 1)), Instant((1, 1, 1))], + ]) +def test_build_instant(arg, expected): + """Returns the expected ``Instant``.""" + + assert periods.instant(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + ["1000-0", ValueError], + ["1000-0-0", ValueError], + ["1000-01-0", ValueError], + ["1000-01-01-01", ValueError], + ["1000-01-1", ValueError], + ["1000-01-32", ValueError], + ["1000-1", ValueError], + ["1000-1-1", ValueError], + ["1000-13", ValueError], + ["month:1000", ValueError], + ["month:1000:1", ValueError], + ["year:1000-01-01", ValueError], + ["year:1000-01-01:1", ValueError], + ["year:1000-01-01:3", ValueError], + [None, TypeError], + [eternity, ValueError], + [year, ValueError], + [Period((day, Instant((1, 1, 1)), 365)), ValueError], + ]) +def test_build_instant_with_an_invalid_argument(arg, error): + """Raises ``ValueError`` when given an invalid argument.""" + + with pytest.raises(error): + periods.instant(arg) + + +@pytest.mark.parametrize("arg, expected", [ + ["1000", Period((year, Instant((1000, 1, 1)), 1))], + ["1000-01", Period((month, Instant((1000, 1, 1)), 1))], + ["1000-01-01", Period((day, Instant((1000, 1, 1)), 1))], + ["1004-02-29", Period((day, Instant((1004, 2, 29)), 1))], + ["ETERNITY", Period((eternity, Instant((1, 1, 1)), 1))], + ["day:1000-01-01", Period((day, Instant((1000, 1, 1)), 1))], + ["day:1000-01-01:3", Period((day, Instant((1000, 1, 1)), 3))], + ["eternity", Period((eternity, Instant((1, 1, 1)), 1))], + ["month:1000-01", Period((month, Instant((1000, 1, 1)), 1))], + ["month:1000-01-01", Period((month, Instant((1000, 1, 1)), 1))], + ["month:1000-01-01:3", Period((month, Instant((1000, 1, 1)), 3))], + ["month:1000-01:3", Period((month, Instant((1000, 1, 1)), 3))], + ["year:1000", Period((year, Instant((1000, 1, 1)), 1))], + ["year:1000-01", Period((year, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01", Period((year, Instant((1000, 1, 1)), 1))], + ["year:1000-01-01:3", Period((year, Instant((1000, 1, 1)), 3))], + ["year:1000-01:3", Period((year, Instant((1000, 1, 1)), 3))], + ["year:1000:3", Period((year, Instant((1000, 1, 1)), 3))], + [1000, Period((year, Instant((1000, 1, 1)), 1))], + [eternity, Period((eternity, Instant((1, 1, 1)), 1))], + [Instant((1, 1, 1)), Period((day, Instant((1, 1, 1)), 1))], + [Period((day, Instant((1, 1, 1)), 365)), Period((day, Instant((1, 1, 1)), 365))], + ]) +def test_build_period(arg, expected): + """Returns the expected ``Period``.""" + + assert periods.period(arg) == expected + + +@pytest.mark.parametrize("arg, error", [ + ["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], + ["day:1000-01", ValueError], + ["day:1000-01:1", ValueError], + ["day:1000:1", ValueError], + ["month:1000", ValueError], + ["month:1000:1", ValueError], + [None, TypeError], + [datetime.date(1, 1, 1), ValueError], + [year, TypeError], + ]) +def test_build_period_with_an_invalid_argument(arg, error): + """Raises ``ValueError`` when given an invalid argument.""" + + with pytest.raises(error): + periods.period(arg) diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index ba1682144a..73c61f4250 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -1,8 +1,6 @@ -import datetime - import pytest -from openfisca_core.periods import DateUnit, Instant, Period +from openfisca_core.periods import DateUnit, Instant day, month, year, eternity = DateUnit @@ -30,47 +28,3 @@ def test_offset(instant, offset, unit, expected): """Returns the expected ``Instant``.""" assert instant.offset(offset, unit) == expected - - -@pytest.mark.parametrize("arg, expected", [ - ["1000", Instant((1000, 1, 1))], - ["1000-01", Instant((1000, 1, 1))], - ["1000-01-01", Instant((1000, 1, 1))], - [1000, Instant((1000, 1, 1))], - [(1000,), Instant((1000, 1, 1))], - [(1000, 1), Instant((1000, 1, 1))], - [(1000, 1, 1), Instant((1000, 1, 1))], - [datetime.date(1, 1, 1), Instant((1, 1, 1))], - [Instant((1, 1, 1)), Instant((1, 1, 1))], - ]) -def test_build_instant(arg, expected): - """Returns the expected ``Instant``.""" - - assert Instant.build(arg) == expected - - -@pytest.mark.parametrize("arg, error", [ - ["1000-0", ValueError], - ["1000-0-0", ValueError], - ["1000-01-0", ValueError], - ["1000-01-01-01", ValueError], - ["1000-01-1", ValueError], - ["1000-01-32", ValueError], - ["1000-1", ValueError], - ["1000-1-1", ValueError], - ["1000-13", ValueError], - ["month:1000", ValueError], - ["month:1000:1", ValueError], - ["year:1000-01-01", ValueError], - ["year:1000-01-01:1", ValueError], - ["year:1000-01-01:3", ValueError], - [None, TypeError], - [eternity, ValueError], - [year, ValueError], - [Period((day, Instant((1, 1, 1)), 365)), ValueError], - ]) -def test_build_instant_with_an_invalid_argument(arg, error): - """Raises ``ValueError`` when given an invalid argument.""" - - with pytest.raises(error): - Instant.build(arg) diff --git a/openfisca_core/periods/tests/test__parsers.py b/openfisca_core/periods/tests/test_parsers.py similarity index 91% rename from openfisca_core/periods/tests/test__parsers.py rename to openfisca_core/periods/tests/test_parsers.py index 9f377265fa..815b405268 100644 --- a/openfisca_core/periods/tests/test__parsers.py +++ b/openfisca_core/periods/tests/test_parsers.py @@ -1,6 +1,8 @@ import pytest -from openfisca_core.periods import DateUnit, ISOFormat +from openfisca_core.periods import DateUnit, isoformat + +year = DateUnit.YEAR @pytest.mark.parametrize("arg, expected", [ @@ -33,7 +35,7 @@ [1., None], [1000, (1000, 1, 1, 4, 1)], [1000., None], - [DateUnit.YEAR, None], + [year, None], [{1, 1, 1, 1}, None], [{1, 1, 1}, None], [{1, 1}, None], @@ -42,7 +44,7 @@ def test_parse_iso_format_from_int(arg, expected): """Returns an ``ISOFormat`` when given a valid ISO format int.""" - assert ISOFormat.fromint(arg) == expected + assert isoformat.fromint(arg) == expected @pytest.mark.parametrize("arg, expected", [ @@ -75,7 +77,7 @@ def test_parse_iso_format_from_int(arg, expected): [1., None], [1000, None], [1000., None], - [DateUnit.YEAR, None], + [year, None], [{1, 1, 1, 1}, None], [{1, 1, 1}, None], [{1, 1}, None], @@ -84,7 +86,7 @@ def test_parse_iso_format_from_int(arg, expected): def test_parse_iso_format_from_str(arg, expected): """Returns an ``ISOFormat`` when given a valid ISO format string.""" - assert ISOFormat.fromstr(arg) == expected + assert isoformat.fromstr(arg) == expected @pytest.mark.parametrize("arg, expected", [ @@ -117,7 +119,7 @@ def test_parse_iso_format_from_str(arg, expected): [1., None], [1000, None], [1000., None], - [DateUnit.YEAR, None], + [year, None], [{1, 1, 1, 1}, None], [{1, 1, 1}, None], [{1, 1}, None], @@ -126,4 +128,4 @@ def test_parse_iso_format_from_str(arg, expected): def test_parse_iso_format_from_seq(arg, expected): """Returns an ``ISOFormat`` when given a valid ISO format sequence.""" - assert ISOFormat.fromseq(arg) == expected + assert isoformat.fromseq(arg) == expected diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index a3477e3ea1..6d30a8a981 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -1,5 +1,3 @@ -import datetime - import pytest from openfisca_core.periods import DateUnit, Instant, Period @@ -134,60 +132,3 @@ def test_day_size_in_days(date_unit, instant, size, expected): period = Period((date_unit, instant, size)) assert period.count(day) == expected - - -@pytest.mark.parametrize("arg, expected", [ - ["1000", Period((year, Instant((1000, 1, 1)), 1))], - ["1000-01", Period((month, Instant((1000, 1, 1)), 1))], - ["1000-01-01", Period((day, Instant((1000, 1, 1)), 1))], - ["1004-02-29", Period((day, Instant((1004, 2, 29)), 1))], - ["ETERNITY", Period((eternity, Instant((1, 1, 1)), 1))], - ["day:1000-01-01", Period((day, Instant((1000, 1, 1)), 1))], - ["day:1000-01-01:3", Period((day, Instant((1000, 1, 1)), 3))], - ["eternity", Period((eternity, Instant((1, 1, 1)), 1))], - ["month:1000-01", Period((month, Instant((1000, 1, 1)), 1))], - ["month:1000-01-01", Period((month, Instant((1000, 1, 1)), 1))], - ["month:1000-01-01:3", Period((month, Instant((1000, 1, 1)), 3))], - ["month:1000-01:3", Period((month, Instant((1000, 1, 1)), 3))], - ["year:1000", Period((year, Instant((1000, 1, 1)), 1))], - ["year:1000-01", Period((year, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01", Period((year, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01:3", Period((year, Instant((1000, 1, 1)), 3))], - ["year:1000-01:3", Period((year, Instant((1000, 1, 1)), 3))], - ["year:1000:3", Period((year, Instant((1000, 1, 1)), 3))], - [1000, Period((year, Instant((1000, 1, 1)), 1))], - [eternity, Period((eternity, Instant((1, 1, 1)), 1))], - [Instant((1, 1, 1)), Period((day, Instant((1, 1, 1)), 1))], - [Period((day, Instant((1, 1, 1)), 365)), Period((day, Instant((1, 1, 1)), 365))], - ]) -def test_build(arg, expected): - """Returns the expected ``Period``.""" - - assert Period.build(arg) == expected - - -@pytest.mark.parametrize("arg, error", [ - ["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], - ["day:1000-01", ValueError], - ["day:1000-01:1", ValueError], - ["day:1000:1", ValueError], - ["month:1000", ValueError], - ["month:1000:1", ValueError], - [None, TypeError], - [datetime.date(1, 1, 1), ValueError], - [year, TypeError], - ]) -def test_build_with_an_invalid_argument(arg, error): - """Raises ``ValueError`` when given an invalid argument.""" - - with pytest.raises(error): - Period.build(arg) diff --git a/openfisca_core/populations/population.py b/openfisca_core/populations/population.py index 708d7cf88d..51ca732556 100644 --- a/openfisca_core/populations/population.py +++ b/openfisca_core/populations/population.py @@ -75,9 +75,9 @@ def check_array_compatible_with_entity( def check_period_validity( self, variable_name: str, - period: periods.period | int | str | None, + period: periods.Period | int | str | None, ) -> None: - if isinstance(period, (int, str, periods.period)): + if isinstance(period, (int, str, periods.Period)): return None stack = traceback.extract_stack() @@ -93,7 +93,7 @@ def check_period_validity( def __call__( self, variable_name: str, - period: periods.period | int | str | None = None, + period: periods.Period | int | str | None = None, options: Optional[Sequence[str]] = None, ) -> Optional[Array[float]]: """ @@ -111,7 +111,7 @@ def __call__( calculate: Calculate = Calculate( variable = variable_name, - period = periods.period.build(period), + period = periods.period(period), option = options, ) @@ -265,7 +265,7 @@ def get_rank( class Calculate(NamedTuple): variable: str - period: periods.period + period: periods.Period option: Sequence[str] | None diff --git a/openfisca_core/scripts/measure_performances.py b/openfisca_core/scripts/measure_performances.py index 4b8459420b..3a7ea4f96d 100644 --- a/openfisca_core/scripts/measure_performances.py +++ b/openfisca_core/scripts/measure_performances.py @@ -186,7 +186,7 @@ def formula(self, simulation, period): @timeit def check_revenu_disponible(year, city_code, expected_revenu_disponible): - simulation = simulations.Simulation(period = periods.period.build(year), tax_benefit_system = tax_benefit_system) + simulation = simulations.Simulation(period = periods.period(year), tax_benefit_system = tax_benefit_system) famille = simulation.populations["famille"] famille.count = 3 famille.roles_count = 2 diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index db4ab9fd85..8b086a0a2e 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -102,8 +102,8 @@ def data_storage_dir(self): def calculate(self, variable_name: str, period): """Calculate ``variable_name`` for ``period``.""" - if period is not None and not isinstance(period, periods.period): - period = periods.period.build(period) + if period is not None and not isinstance(period, periods.Period): + period = periods.period(period) self.tracer.record_calculation_start(variable_name, period) @@ -116,7 +116,7 @@ def calculate(self, variable_name: str, period): self.tracer.record_calculation_end() self.purge_cache_of_invalid_values() - def _calculate(self, variable_name: str, period: periods.period): + def _calculate(self, variable_name: str, period: periods.Period): """ Calculate the variable ``variable_name`` for the period ``period``, using the variable formula if it exists. @@ -174,8 +174,8 @@ def calculate_add(self, variable_name: str, period): if variable is None: raise VariableNotFoundError(variable_name, self.tax_benefit_system) - if period is not None and not isinstance(period, periods.period): - period = periods.period.build(period) + if period is not None and not isinstance(period, periods.Period): + period = periods.period(period) # Check that the requested period matches definition_period if variable.definition_period > period.unit: @@ -203,8 +203,8 @@ def calculate_divide(self, variable_name: str, period): if variable is None: raise VariableNotFoundError(variable_name, self.tax_benefit_system) - if period is not None and not isinstance(period, periods.period): - period = periods.period.build(period) + if period is not None and not isinstance(period, periods.Period): + period = periods.period(period) # Check that the requested period matches definition_period if variable.definition_period != periods.YEAR: @@ -351,8 +351,8 @@ 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, periods.period): - period = periods.period.build(period) + if period is not None and not isinstance(period, periods.Period): + period = periods.period(period) return self.get_holder(variable_name).get_array(period) def get_holder(self, variable_name: str): @@ -445,7 +445,7 @@ def set_input(self, variable_name: str, period, value): if variable is None: raise VariableNotFoundError(variable_name, self.tax_benefit_system) - period = periods.period.build(period) + period = periods.period(period) if ((variable.end is not None) and (period.start.date() > variable.end)): return self.get_holder(variable_name).set_input(period, value) @@ -501,4 +501,4 @@ def clone(self, debug = False, trace = False): class Cache(NamedTuple): variable: str - period: periods.period + period: periods.Period diff --git a/openfisca_core/simulations/simulation_builder.py b/openfisca_core/simulations/simulation_builder.py index b10cd68c27..41c9e4f8da 100644 --- a/openfisca_core/simulations/simulation_builder.py +++ b/openfisca_core/simulations/simulation_builder.py @@ -325,7 +325,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.build(period_str)) + self.default_period = str(periods.period(period_str)) def get_input(self, variable, period_str): if variable not in self.input_buffer: @@ -368,7 +368,7 @@ def init_variable_values(self, entity, instance_object, instance_id): for period_str, value in variable_values.items(): try: - periods.period.build(period_str) + periods.period(period_str) except ValueError as e: raise SituationParsingError(path_in_json, e.args[0]) variable = entity.get_variable(variable_name) @@ -393,7 +393,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.build(period_str))] = array + self.input_buffer[variable.name][str(periods.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 +411,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.build(period_str) for period_str in self.input_buffer[variable_name].keys()] + unsorted_periods = [periods.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 = lambda period: f"{period.unit}_{period.size}") diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 38349bd916..786773b273 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -45,7 +45,7 @@ class TaxBenefitSystem: person_entity: Entity _base_tax_benefit_system = None - _parameters_at_instant_cache: Dict[periods.instant, types.ParameterNodeAtInstant] = {} + _parameters_at_instant_cache: Dict[periods.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. @@ -344,7 +344,7 @@ def neutralize_variable(self, variable_name: str): def annualize_variable( self, variable_name: str, - period: periods.period | None = None, + period: periods.Period | None = None, ) -> None: check: bool variable: Optional[Variable] @@ -387,7 +387,7 @@ def _get_baseline_parameters_at_instant(self, instant): @functools.lru_cache() def get_parameters_at_instant( self, - instant: periods.period | periods.instant | str | int, + instant: periods.Period | periods.Instant | str | int, ) -> Optional[types.ParameterNodeAtInstant]: """Get the parameters of the legislation at a given instant @@ -399,14 +399,14 @@ def get_parameters_at_instant( """ - if isinstance(instant, periods.instant): + if isinstance(instant, periods.Instant): key = instant - elif isinstance(instant, periods.period): + elif isinstance(instant, periods.Period): key = instant.start elif isinstance(instant, (str, int)): - key = periods.instant.build(instant) + key = periods.instant(instant) else: msg = f"Expected an Instant (e.g. Instant((2017, 1, 1)) ). Got: {instant}." diff --git a/openfisca_core/tracers/full_tracer.py b/openfisca_core/tracers/full_tracer.py index 6edb42d937..d1a8a3a15f 100644 --- a/openfisca_core/tracers/full_tracer.py +++ b/openfisca_core/tracers/full_tracer.py @@ -9,7 +9,7 @@ from .. import tracers -Stack = List[Dict[str, Union[str, periods.period]]] +Stack = List[Dict[str, Union[str, periods.Period]]] class FullTracer: @@ -26,7 +26,7 @@ def __init__(self) -> None: def record_calculation_start( self, variable: str, - period: periods.period, + period: periods.Period, ) -> None: self._simple_tracer.record_calculation_start(variable, period) self._enter_calculation(variable, period) @@ -35,7 +35,7 @@ def record_calculation_start( def _enter_calculation( self, variable: str, - period: periods.period, + period: periods.Period, ) -> None: new_node = tracers.TraceNode( name = variable, @@ -54,7 +54,7 @@ def _enter_calculation( def record_parameter_access( self, parameter: str, - period: periods.period, + period: periods.Period, value: ArrayLike, ) -> None: diff --git a/openfisca_core/tracers/simple_tracer.py b/openfisca_core/tracers/simple_tracer.py index ab0de1349d..61b40d2902 100644 --- a/openfisca_core/tracers/simple_tracer.py +++ b/openfisca_core/tracers/simple_tracer.py @@ -5,7 +5,7 @@ from openfisca_core import periods -Stack = List[Dict[str, Union[str, periods.period]]] +Stack = List[Dict[str, Union[str, periods.Period]]] class SimpleTracer: @@ -15,7 +15,7 @@ class SimpleTracer: def __init__(self) -> None: self._stack = [] - def record_calculation_start(self, variable: str, period: periods.period) -> None: + def record_calculation_start(self, variable: str, period: periods.Period) -> None: self.stack.append({'name': variable, 'period': period}) def record_calculation_result(self, value: ArrayLike) -> None: diff --git a/openfisca_core/tracers/trace_node.py b/openfisca_core/tracers/trace_node.py index 753d3c33b7..0edf798482 100644 --- a/openfisca_core/tracers/trace_node.py +++ b/openfisca_core/tracers/trace_node.py @@ -12,7 +12,7 @@ @dataclasses.dataclass class TraceNode: name: str - period: periods.period + period: periods.Period parent: TraceNode | None = None children: List[TraceNode] = dataclasses.field(default_factory = list) parameters: List[TraceNode] = dataclasses.field(default_factory = list) diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index 283840db6b..931af68351 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -9,7 +9,7 @@ from .. import variables -def get_annualized_variable(variable: variables.Variable, annualization_period: Optional[periods.period] = None) -> variables.Variable: +def get_annualized_variable(variable: variables.Variable, annualization_period: Optional[periods.Period] = None) -> variables.Variable: """ Returns a clone of ``variable`` that is annualized for the period ``annualization_period``. When annualized, a variable's formula is only called for a January calculation, and the results for other months are assumed to be identical. diff --git a/openfisca_core/variables/variable.py b/openfisca_core/variables/variable.py index 21b757cc30..4c1460d756 100644 --- a/openfisca_core/variables/variable.py +++ b/openfisca_core/variables/variable.py @@ -309,7 +309,7 @@ def get_introspection_data(cls, tax_benefit_system): def get_formula( self, - period: periods.period | periods.instant | str | int | None = None, + period: periods.Period | periods.Instant | str | int | None = None, ) -> Formula | None: """Returns the formula to compute the variable at the given period. @@ -330,15 +330,15 @@ def get_formula( if period is None: return self.formulas.peekitem(index = 0)[1] # peekitem gets the 1st key-value tuple (the oldest start_date and formula). Return the formula. - if isinstance(period, periods.period): + if isinstance(period, periods.Period): instant = period.start else: try: - instant = periods.period.build(period).start + instant = periods.period(period).start except ValueError: - instant = periods.instant.build(period) + instant = periods.instant(period) if instant is None: return None diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index 16219c7257..dd0848b40a 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -5,7 +5,7 @@ from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable -PERIOD = periods.period.build("2016-01") +PERIOD = periods.period("2016-01") @pytest.mark.parametrize("simulation", [({"salary": 2000}, PERIOD)], indirect = True) diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index b7aee1e852..d1a4e6c358 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.build('2013-01') + return periods.period('2013-01') @pytest.fixture diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index bad6c4a5d4..907aefceb5 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -26,7 +26,7 @@ def couple(tax_benefit_system): build_from_entities(tax_benefit_system, situation_examples.couple) -period = periods.period.build('2017-12') +period = periods.period('2017-12') def test_set_input_enum_string(couple): @@ -89,7 +89,7 @@ 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.build(periods.ETERNITY), value) + holder.set_input(periods.period(periods.ETERNITY), value) assert holder.get_array(None) == value assert holder.get_array(periods.ETERNITY) == value assert holder.get_array('2016-01') == value @@ -98,8 +98,8 @@ def test_permanent_variable_filled(single): def test_delete_arrays(single): simulation = single salary_holder = simulation.person.get_holder('salary') - salary_holder.set_input(periods.period.build(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.period.build(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.period(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.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 +109,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.build(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.period(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -119,7 +119,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.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.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 +132,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.build(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.period(2017), numpy.asarray([30000])) simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-01') simulation.calculate('salary', '2017-02') @@ -147,7 +147,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.build(2019), 'owner') + holders.set_input_dispatch_by_period(holder, periods.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 +159,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.build(2017), numpy.asarray([30000])) - salary_holder.set_input(periods.period.build(2018), numpy.asarray([60000])) + salary_holder.set_input(periods.period(2017), numpy.asarray([30000])) + salary_holder.set_input(periods.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.build(2018), numpy.asarray([15000])) + salary_holder.set_input(periods.period(2018), numpy.asarray([15000])) assert simulation.person('salary', '2017-01') == 2500 assert simulation.person('salary', '2018-01') == 1250 @@ -172,7 +172,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.build('2017-01') + month = periods.period('2017-01') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -183,8 +183,8 @@ def test_cache_disk(couple): def test_known_periods(couple): simulation = couple simulation.memory_config = force_storage_on_disk - month = periods.period.build('2017-01') - month_2 = periods.period.build('2017-02') + month = periods.period('2017-01') + month_2 = periods.period('2017-02') holder = simulation.person.get_holder('disposable_income') data = numpy.asarray([2000, 3000]) holder.put_in_cache(data, month) @@ -196,7 +196,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.build('2017-01') + month = periods.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 4ec9251224..b4eab3e5a5 100644 --- a/tests/core/test_opt_out_cache.py +++ b/tests/core/test_opt_out_cache.py @@ -7,7 +7,7 @@ from openfisca_core.variables import Variable -PERIOD = periods.period.build("2016-01") +PERIOD = periods.period("2016-01") class input(Variable): diff --git a/tests/core/test_reforms.py b/tests/core/test_reforms.py index aac5aaab49..15fe78b3d6 100644 --- a/tests/core/test_reforms.py +++ b/tests/core/test_reforms.py @@ -124,23 +124,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.build(2013).start, - periods.period.build(2013).stop, + periods.period(2013).start, + periods.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.build(2015).start, - periods.period.build(2015).stop, + periods.period(2015).start, + periods.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.build(2013).start, + periods.period(2013).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2013-01-01": {'value': 1.0}}), @@ -148,15 +148,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.build(2011).start, - periods.period.build(2011).stop, + periods.period(2011).start, + periods.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.build(2015).start, + periods.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 +164,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.build(2014).start, - periods.period.build(2014).stop, + periods.period(2014).start, + periods.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.build(2014).start, + periods.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 +180,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.build(2005).start, - periods.period.build(2005).stop, + periods.period(2005).start, + periods.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.build(2003).start, - periods.period.build(2003).stop, + periods.period(2003).start, + periods.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.build(2005).start, + periods.period(2005).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2005-01-01": {'value': 1.0}}), @@ -204,7 +204,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.build(2006).start, + periods.period(2006).start, None, # stop instant 1.0, ValuesHistory('dummy_name', {"2006-01-01": {'value': 1.0}}), diff --git a/tests/core/variables/test_annualize.py b/tests/core/variables/test_annualize.py index e48d387dd1..4db394b761 100644 --- a/tests/core/variables/test_annualize.py +++ b/tests/core/variables/test_annualize.py @@ -41,7 +41,7 @@ def __call__(self, variable_name: str, period): def test_without_annualize(monthly_variable): - period = periods.period.build(2019) + period = periods.period(2019) person = PopulationMock(monthly_variable) @@ -55,7 +55,7 @@ def test_without_annualize(monthly_variable): def test_with_annualize(monthly_variable): - period = periods.period.build(2019) + period = periods.period(2019) annualized_variable = get_annualized_variable(monthly_variable) person = PopulationMock(annualized_variable) @@ -70,8 +70,8 @@ def test_with_annualize(monthly_variable): def test_with_partial_annualize(monthly_variable): - period = periods.period.build('year:2018:2') - annualized_variable = get_annualized_variable(monthly_variable, periods.period.build(2018)) + period = periods.period('year:2018:2') + annualized_variable = get_annualized_variable(monthly_variable, periods.period(2018)) person = PopulationMock(annualized_variable) From dd523b61a136f1b7430cd99d4b13c6a727db7942 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 19 Dec 2022 11:53:18 +0100 Subject: [PATCH 82/93] Reuse parsers to simplify period build --- openfisca_core/periods/_builders.py | 67 +------- openfisca_core/periods/_iso_format.py | 4 +- openfisca_core/periods/_parsers.py | 148 +++++++++++++----- openfisca_core/periods/tests/test_builders.py | 10 +- openfisca_core/periods/tests/test_parsers.py | 80 +++++++--- 5 files changed, 179 insertions(+), 130 deletions(-) diff --git a/openfisca_core/periods/_builders.py b/openfisca_core/periods/_builders.py index 241cea0912..e881149e23 100644 --- a/openfisca_core/periods/_builders.py +++ b/openfisca_core/periods/_builders.py @@ -13,7 +13,7 @@ ) from ._instant import Instant from ._iso_format import ISOFormat -from ._parsers import fromint, fromseq, fromstr +from ._parsers import fromcomplex, fromint, fromseq, fromstr from ._period import Period day, _month, year, eternity = tuple(DateUnit) @@ -130,10 +130,6 @@ def period(value: Any) -> Period: """ - unit: DateUnit | int | str - part: ISOFormat | None - size: int | str - if value in {eternity, eternity.name, eternity.name.lower()}: return Period((eternity, instant(datetime.date.min), 1)) @@ -152,63 +148,12 @@ def period(value: Any) -> Period: if not isinstance(value, str): raise PeriodFormatError(value) - # Try to parse as a simple period - part = fromstr(value) - - if part is not None: - start = Instant((part.year, part.month, part.day)) - return Period((DateUnit(part.unit), start, 1)) - - # Complex periods must have a ':' in their strings - if ":" not in value: - raise PeriodFormatError(value) - - # We know the first element has to be a ``unit`` - unit, *rest = value.split(":") - - # Units are case insensitive so we need to upper them - unit = unit.upper() - - # Left-most component must be a valid unit - if unit not in dir(DateUnit): - raise PeriodFormatError(value) - - unit = DateUnit[unit] - - # We get the first remaining component - date, *rest = rest - - if date is None: - raise PeriodFormatError(value) + # Try to parse as a complex period + isoformat = fromcomplex(value) - # Middle component must be a valid ISO period - part = fromstr(date) - - if part is None: - raise PeriodFormatError(value) - - # Finally we try to parse the size, if any - try: - size, *rest = rest - - except ValueError: - size = 1 - - # If provided, make sure the size is an integer - try: - size = int(size) - - except ValueError: - raise PeriodFormatError(value) - - # If there were more than 2 ":" in the string, the period is invalid - if len(rest) > 0: - raise PeriodFormatError(value) - - # Reject ambiguous periods such as month:2014 - if part.unit > unit: + if isoformat is None: raise PeriodFormatError(value) - start = Instant((part.year, part.month, part.day)) + unit = DateUnit(isoformat.unit) - return Period((unit, start, size)) + return Period((unit, instant(isoformat[:3]), isoformat.size)) diff --git a/openfisca_core/periods/_iso_format.py b/openfisca_core/periods/_iso_format.py index 0fb7ab834b..36be7638b7 100644 --- a/openfisca_core/periods/_iso_format.py +++ b/openfisca_core/periods/_iso_format.py @@ -18,5 +18,5 @@ class ISOFormat(NamedTuple): #: The unit of the parsed period, in binary. unit: int - #: The number of fragments in the parsed period. - shape: int + #: The size of the parsed instant or period. + size: int diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py index 454715c560..e2e6cd013a 100644 --- a/openfisca_core/periods/_parsers.py +++ b/openfisca_core/periods/_parsers.py @@ -6,6 +6,7 @@ from pendulum.datetime import Date from pendulum.parsing import ParserError +from ._date_unit import DateUnit from ._iso_format import ISOFormat @@ -21,10 +22,10 @@ def fromint(value: int) -> ISOFormat | None: Examples: >>> fromint(1) - ISOFormat(year=1, month=1, day=1, unit=4, shape=1) + ISOFormat(year=1, month=1, day=1, unit=4, size=1) >>> fromint(2023) - ISOFormat(year=2023, month=1, day=1, unit=4, shape=1) + ISOFormat(year=2023, month=1, day=1, unit=4, size=1) >>> fromint(-1) @@ -52,6 +53,74 @@ def fromint(value: int) -> ISOFormat | None: return ISOFormat(value, 1, 1, 4, 1) +def fromseq(value: Sequence[int]) -> ISOFormat | None: + """Parse a sequence of ints respecting the ISO format. + + Args: + value: A sequence of ints such as [2012, 3, 13]. + + Returns: + An ISOFormat object if ``value`` is valid. + None if ``value`` is not valid. + + Examples: + >>> fromseq([2022]) + ISOFormat(year=2022, month=1, day=1, unit=4, size=1) + + >>> fromseq([2022, 1]) + ISOFormat(year=2022, month=1, day=1, unit=2, size=1) + + >>> fromseq([2022, 1, 1]) + ISOFormat(year=2022, month=1, day=1, unit=1, size=1) + + >>> fromseq([-2022, 1, 1]) + + >>> fromseq([2022, 13, 1]) + + >>> fromseq([2022, 1, 32]) + + .. versionadded:: 39.0.0 + + """ + + if not isinstance(value, (list, tuple)): + return None + + if not value: + return None + + if not 1 <= len(value) <= 3: + return None + + if not all(isinstance(unit, int) for unit in value): + return None + + if not all(unit == abs(unit) for unit in value): + return None + + # We get the shape of the string (e.g. "2012-02" = 2) + shape = len(value) + + # We get the unit from the shape (e.g. 2 = "month") + unit = tuple(DateUnit)[3 - shape] + + while len(value) < 3: + value = (*value, 1) + + try: + # We parse the date + date = pendulum.date(*value) + + except ValueError: + return None + + if not isinstance(date, Date): + return None + + # We build the corresponding ISOFormat object + return ISOFormat(date.year, date.month, date.day, unit.value, 1) + + def fromstr(value: str) -> ISOFormat | None: """Parse strings respecting the ISO format. @@ -64,13 +133,13 @@ def fromstr(value: str) -> ISOFormat | None: Examples: >>> fromstr("2022") - ISOFormat(year=2022, month=1, day=1, unit=4, shape=1) + ISOFormat(year=2022, month=1, day=1, unit=4, size=1) >>> fromstr("2022-02") - ISOFormat(year=2022, month=2, day=1, unit=2, shape=2) + ISOFormat(year=2022, month=2, day=1, unit=2, size=1) >>> fromstr("2022-02-13") - ISOFormat(year=2022, month=2, day=13, unit=1, shape=3) + ISOFormat(year=2022, month=2, day=13, unit=1, size=1) >>> fromstr(1000) @@ -108,75 +177,68 @@ def fromstr(value: str) -> ISOFormat | None: shape = len(value.split("-")) # We get the unit from the shape (e.g. 2 = "month") - unit = pow(2, 3 - shape) + unit = tuple(DateUnit)[3 - shape] # We build the corresponding ISOFormat object - return ISOFormat(date.year, date.month, date.day, unit, shape) + return ISOFormat(date.year, date.month, date.day, unit.value, 1) -def fromseq(value: Sequence[int]) -> ISOFormat | None: - """Parse a sequence of ints respecting the ISO format. +def fromcomplex(value: str) -> ISOFormat | None: + """Parse complex strings representing periods. Args: - value: A sequence of ints such as [2012, 3, 13]. + value: A string such as such as "year:2012" or "month:2015-03:12". Returns: An ISOFormat object if ``value`` is valid. None if ``value`` is not valid. Examples: - >>> fromseq([2022]) - ISOFormat(year=2022, month=1, day=1, unit=4, shape=1) - - >>> fromseq([2022, 1]) - ISOFormat(year=2022, month=1, day=1, unit=2, shape=2) + >>> fromcomplex("year:2022") + ISOFormat(year=2022, month=1, day=1, unit=4, size=1) - >>> fromseq([2022, 1, 1]) - ISOFormat(year=2022, month=1, day=1, unit=1, shape=3) + >>> fromcomplex("month:2022-02") + ISOFormat(year=2022, month=2, day=1, unit=2, size=1) - >>> fromseq([-2022, 1, 1]) + >>> fromcomplex("day:2022-02-13:15") + ISOFormat(year=2022, month=2, day=13, unit=1, size=15) - >>> fromseq([2022, 13, 1]) + >>> fromcomplex("2022:3") - >>> fromseq([2022, 1, 32]) + >>> fromcomplex("ETERNITY") .. versionadded:: 39.0.0 """ - if not isinstance(value, (list, tuple)): + if not isinstance(value, str): return None if not value: return None - if not 1 <= len(value) <= 3: - return None + # If it is not a complex value, delegate! + if len(value.split(":")) == 1: + return fromstr(value) - if not all(isinstance(unit, int) for unit in value): - return None + first, second, *rest = value.split(":") + unit = DateUnit.__members__.get(first.upper()) + date = fromstr(second) - if not all(unit == abs(unit) for unit in value): + # If it is an invalid unit, next! + if unit is None: return None - # We get the shape of the string (e.g. "2012-02" = 2) - shape = len(value) - - # We get the unit from the shape (e.g. 2 = "month") - unit = pow(2, 3 - shape) - - while len(value) < 3: - value = (*value, 1) - - try: - # We parse the date - date = pendulum.date(*value) - - except ValueError: + # If it is an invalid date, next! + if date is None: return None - if not isinstance(date, Date): - return None + # If it has no size, we'll assume ``1`` + if not rest: + size = 1 + + else: + size = int(rest[0]) # We build the corresponding ISOFormat object - return ISOFormat(date.year, date.month, date.day, unit, shape) + return ISOFormat(date.year, date.month, date.day, unit.value, size) diff --git a/openfisca_core/periods/tests/test_builders.py b/openfisca_core/periods/tests/test_builders.py index 9227d92003..aa87280bca 100644 --- a/openfisca_core/periods/tests/test_builders.py +++ b/openfisca_core/periods/tests/test_builders.py @@ -75,6 +75,11 @@ def test_build_instant_with_an_invalid_argument(arg, error): [eternity, Period((eternity, Instant((1, 1, 1)), 1))], [Instant((1, 1, 1)), Period((day, Instant((1, 1, 1)), 1))], [Period((day, Instant((1, 1, 1)), 365)), Period((day, Instant((1, 1, 1)), 365))], + ["month:1000:1", Period((2, Instant((1000, 1, 1)), 1))], + ["month:1000", Period((2, Instant((1000, 1, 1)), 1))], + ["day:1000:1", Period((1, Instant((1000, 1, 1)), 1))], + ["day:1000-01:1", Period((1, Instant((1000, 1, 1)), 1))], + ["day:1000-01", Period((1, Instant((1000, 1, 1)), 1))], ]) def test_build_period(arg, expected): """Returns the expected ``Period``.""" @@ -93,11 +98,6 @@ def test_build_period(arg, expected): ["1000-13", ValueError], ["1000-2-31", ValueError], ["1000:1", ValueError], - ["day:1000-01", ValueError], - ["day:1000-01:1", ValueError], - ["day:1000:1", ValueError], - ["month:1000", ValueError], - ["month:1000:1", ValueError], [None, TypeError], [datetime.date(1, 1, 1), ValueError], [year, TypeError], diff --git a/openfisca_core/periods/tests/test_parsers.py b/openfisca_core/periods/tests/test_parsers.py index 815b405268..b6f462d611 100644 --- a/openfisca_core/periods/tests/test_parsers.py +++ b/openfisca_core/periods/tests/test_parsers.py @@ -47,11 +47,53 @@ def test_parse_iso_format_from_int(arg, expected): assert isoformat.fromint(arg) == expected +@pytest.mark.parametrize("arg, expected", [ + ["1", None], + ["1000", None], + ["1000-01", None], + ["1000-01-01", None], + ["1000-01-1", None], + ["1000-01-99", None], + ["1000-1", None], + ["1000-1-1", None], + ["999", None], + ["eternity", None], + ["first-of", None], + ["year:2021:7", None], + [(1, 1), (1, 1, 1, 2, 1)], + [(1, 1, 1), (1, 1, 1, 1, 1)], + [(1, 1, 1, 1), None], + [(1,), (1, 1, 1, 4, 1)], + [(2022, 1), (2022, 1, 1, 2, 1)], + [(2022, 1, 1), (2022, 1, 1, 1, 1)], + [(2022, 12), (2022, 12, 1, 2, 1)], + [(2022, 12, 1), (2022, 12, 1, 1, 1)], + [(2022, 12, 31), (2022, 12, 31, 1, 1)], + [(2022, 12, 32), None], + [(2022, 13), None], + [(2022, 13, 31), None], + [(2022,), (2022, 1, 1, 4, 1)], + [1, None], + [1., None], + [1000, None], + [1000., None], + [year, None], + [{1, 1, 1, 1}, None], + [{1, 1, 1}, None], + [{1, 1}, None], + [{1}, None], + ]) +def test_parse_iso_format_from_seq(arg, expected): + """Returns an ``ISOFormat`` when given a valid ISO format sequence.""" + + assert isoformat.fromseq(arg) == expected + + @pytest.mark.parametrize("arg, expected", [ ["1", None], ["1000", (1000, 1, 1, 4, 1)], - ["1000-01", (1000, 1, 1, 2, 2)], - ["1000-01-01", (1000, 1, 1, 1, 3)], + ["1000-01", (1000, 1, 1, 2, 1)], + ["1000-01-01", (1000, 1, 1, 1, 1)], ["1000-01-1", None], ["1000-01-99", None], ["1000-1", None], @@ -91,9 +133,9 @@ def test_parse_iso_format_from_str(arg, expected): @pytest.mark.parametrize("arg, expected", [ ["1", None], - ["1000", None], - ["1000-01", None], - ["1000-01-01", None], + ["1000", (1000, 1, 1, 4, 1)], + ["1000-01", (1000, 1, 1, 2, 1)], + ["1000-01-01", (1000, 1, 1, 1, 1)], ["1000-01-1", None], ["1000-01-99", None], ["1000-1", None], @@ -101,20 +143,20 @@ def test_parse_iso_format_from_str(arg, expected): ["999", None], ["eternity", None], ["first-of", None], - ["year:2021:7", None], - [(1, 1), (1, 1, 1, 2, 2)], - [(1, 1, 1), (1, 1, 1, 1, 3)], + ["year:2021:7", (2021, 1, 1, 4, 7)], + [(1, 1), None], + [(1, 1, 1), None], [(1, 1, 1, 1), None], - [(1,), (1, 1, 1, 4, 1)], - [(2022, 1), (2022, 1, 1, 2, 2)], - [(2022, 1, 1), (2022, 1, 1, 1, 3)], - [(2022, 12), (2022, 12, 1, 2, 2)], - [(2022, 12, 1), (2022, 12, 1, 1, 3)], - [(2022, 12, 31), (2022, 12, 31, 1, 3)], + [(1,), None], + [(2022, 1), None], + [(2022, 1, 1), None], + [(2022, 12), None], + [(2022, 12, 1), None], + [(2022, 12, 31), None], [(2022, 12, 32), None], [(2022, 13), None], [(2022, 13, 31), None], - [(2022,), (2022, 1, 1, 4, 1)], + [(2022,), None], [1, None], [1., None], [1000, None], @@ -123,9 +165,9 @@ def test_parse_iso_format_from_str(arg, expected): [{1, 1, 1, 1}, None], [{1, 1, 1}, None], [{1, 1}, None], - [{1}, None], + [{1, }, None], ]) -def test_parse_iso_format_from_seq(arg, expected): - """Returns an ``ISOFormat`` when given a valid ISO format sequence.""" +def test_parse_iso_format_from_complex_str(arg, expected): + """Returns an ``ISOFormat`` when given a valid complex period.""" - assert isoformat.fromseq(arg) == expected + assert isoformat.fromcomplex(arg) == expected From 091fe10d5ec96973f5f48c2d95e8fa20b3236b23 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Mon, 19 Dec 2022 22:42:44 +0100 Subject: [PATCH 83/93] Improved period factory --- openfisca_core/periods/_period.py | 113 ++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 22 deletions(-) diff --git a/openfisca_core/periods/_period.py b/openfisca_core/periods/_period.py index 3605ec4fb9..fb350049b5 100644 --- a/openfisca_core/periods/_period.py +++ b/openfisca_core/periods/_period.py @@ -358,18 +358,17 @@ def this(self, unit: DateUnit) -> Period: >>> period.this(YEAR) Period((year, Instant((2023, 1, 1)), 1)) - .. versionadded:: 39.0.0 """ return type(self)((unit, self.start.offset("first-of", unit), 1)) - def last(self, unit: DateUnit, size: int = 1) -> Period: - """Last ``size`` ``unit``s of the ``Period``. + def come(self, unit: DateUnit, size: int = 1) -> Period: + """The next ``unit``s ``size`` from ``Period.start``. Args: unit: The unit of the requested Period. - size: The number of units to include in the Period. + size: The number of units ago. Returns: A Period. @@ -379,32 +378,30 @@ def last(self, unit: DateUnit, size: int = 1) -> Period: >>> period = Period((YEAR, start, 3)) - >>> period.last(DAY) - Period((day, Instant((2022, 12, 31)), 1)) + >>> period.come(DAY) + Period((day, Instant((2023, 1, 2)), 1)) - >>> period.last(DAY, 7) - Period((day, Instant((2022, 12, 25)), 7)) + >>> period.come(DAY, 7) + Period((day, Instant((2023, 1, 8)), 1)) - >>> period.last(MONTH) - Period((month, Instant((2022, 12, 1)), 1)) + >>> period.come(MONTH) + Period((month, Instant((2023, 2, 1)), 1)) - >>> period.last(MONTH, 3) - Period((month, Instant((2022, 10, 1)), 3)) + >>> period.come(MONTH, 3) + Period((month, Instant((2023, 4, 1)), 1)) - >>> period.last(YEAR) - Period((year, Instant((2022, 1, 1)), 1)) - - >>> period.last(YEAR, 1) - Period((year, Instant((2022, 1, 1)), 1)) + >>> period.come(YEAR) + Period((year, Instant((2024, 1, 1)), 1)) - .. versionadded:: 39.0.0 + >>> period.come(YEAR, 1) + Period((year, Instant((2024, 1, 1)), 1)) """ - return type(self)((unit, self.ago(unit, size).start, size)) + return type(self)((unit, self.this(unit).start, 1)).offset(size) def ago(self, unit: DateUnit, size: int = 1) -> Period: - """``size`` ``unit``s ago of the ``Period``. + """``size`` ``unit``s ago from ``Period.start``. Args: unit: The unit of the requested Period. @@ -436,11 +433,83 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: >>> period.ago(YEAR, 1) Period((year, Instant((2022, 1, 1)), 1)) - .. versionadded:: 39.0.0 + """ + + return self.come(unit, -size) + + def until(self, unit: DateUnit, size: int = 1) -> Period: + """Next ``unit`` ``size``s from ``Period.start``. + + Args: + unit: The unit of the requested Period. + size: The number of units to include in the Period. + + Returns: + A Period. + + Examples: + >>> start = Instant((2023, 1, 1)) + + >>> period = Period((YEAR, start, 3)) + + >>> period.until(DAY) + Period((day, Instant((2023, 1, 1)), 1)) + + >>> period.until(DAY, 7) + Period((day, Instant((2023, 1, 1)), 7)) + + >>> period.until(MONTH) + Period((month, Instant((2023, 1, 1)), 1)) + + >>> period.until(MONTH, 3) + Period((month, Instant((2023, 1, 1)), 3)) + + >>> period.until(YEAR) + Period((year, Instant((2023, 1, 1)), 1)) + + >>> period.until(YEAR, 1) + Period((year, Instant((2023, 1, 1)), 1)) + + """ + + return type(self)((unit, self.this(unit).start, size)) + + def last(self, unit: DateUnit, size: int = 1) -> Period: + """Last ``size`` ``unit``s from ``Period.start``. + + Args: + unit: The unit of the requested Period. + size: The number of units to include in the Period. + + Returns: + A Period. + + Examples: + >>> start = Instant((2023, 1, 1)) + + >>> period = Period((YEAR, start, 3)) + + >>> period.last(DAY) + Period((day, Instant((2022, 12, 31)), 1)) + + >>> period.last(DAY, 7) + Period((day, Instant((2022, 12, 25)), 7)) + + >>> period.last(MONTH) + Period((month, Instant((2022, 12, 1)), 1)) + + >>> period.last(MONTH, 3) + Period((month, Instant((2022, 10, 1)), 3)) + + >>> period.last(YEAR) + Period((year, Instant((2022, 1, 1)), 1)) + + >>> period.last(YEAR, 1) + Period((year, Instant((2022, 1, 1)), 1)) """ - return type(self)((unit, self.this(unit).start, 1)).offset(-size) + return type(self)((unit, self.ago(unit, size).start, size)) def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: """Increment (or decrement) the given period with offset units. From 7e1d3b36bd19ffad123b9c145700c86c31036fbe Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 20 Dec 2022 00:19:00 +0100 Subject: [PATCH 84/93] Improve packaging --- CHANGELOG.md | 2 +- openfisca_core/periods/__init__.py | 12 +- openfisca_core/periods/_builders.py | 159 ------- .../periods/{_date_unit.py => _dates.py} | 23 +- .../periods/{_exceptions.py => _errors.py} | 0 openfisca_core/periods/_helpers.py | 395 ++++++++++++++++++ .../periods/{_instant.py => _instants.py} | 34 +- openfisca_core/periods/_iso_format.py | 22 - openfisca_core/periods/_parsers.py | 244 ----------- .../periods/{_period.py => _periods.py} | 6 +- openfisca_core/periods/_utils.py | 31 -- openfisca_core/periods/tests/test_parsers.py | 24 +- setup.cfg | 1 - 13 files changed, 469 insertions(+), 484 deletions(-) delete mode 100644 openfisca_core/periods/_builders.py rename openfisca_core/periods/{_date_unit.py => _dates.py} (86%) rename openfisca_core/periods/{_exceptions.py => _errors.py} (100%) create mode 100644 openfisca_core/periods/_helpers.py rename openfisca_core/periods/{_instant.py => _instants.py} (86%) delete mode 100644 openfisca_core/periods/_iso_format.py delete mode 100644 openfisca_core/periods/_parsers.py rename openfisca_core/periods/{_period.py => _periods.py} (99%) delete mode 100644 openfisca_core/periods/_utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f74a4b75..4460b5ca20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ ##### Deprecations - Deprecate `INSTANT_PATTERN` - - The feature is now provided by `periods.isoformat.fromstr` + - The feature is now provided by `periods.parse` - Deprecate `instant_date`. - The feature is now provided by `periods.instant.date`. - Deprecate `periods.{unit_weight, unit_weights, key_period_size}`. diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index fcf29f3657..ce8e2cb5a7 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -27,12 +27,12 @@ """ -from . import _parsers as isoformat -from ._builders import instant, period -from ._date_unit import DateUnit -from ._instant import Instant -from ._parsers import fromstr as parse -from ._period import Period +from ._dates import DateUnit +from ._helpers import build_instant as instant +from ._helpers import build_period as period +from ._helpers import parse_instant_str as parse +from ._instants import Instant +from ._periods import Period DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) diff --git a/openfisca_core/periods/_builders.py b/openfisca_core/periods/_builders.py deleted file mode 100644 index e881149e23..0000000000 --- a/openfisca_core/periods/_builders.py +++ /dev/null @@ -1,159 +0,0 @@ -from __future__ import annotations - -from typing import Any - -import datetime - -from ._date_unit import DateUnit -from ._exceptions import ( - InstantFormatError, - InstantTypeError, - PeriodFormatError, - PeriodTypeError, - ) -from ._instant import Instant -from ._iso_format import ISOFormat -from ._parsers import fromcomplex, fromint, fromseq, fromstr -from ._period import Period - -day, _month, year, eternity = tuple(DateUnit) - - -def instant(value: Any) -> Instant: - """Build a new instant, aka a triple of integers (year, month, day). - - Args: - value: An ``instant-like`` object. - - Returns: - An Instant. - - Raises: - InstantFormatError: When ``value`` is invalid, like "2021-32-13". - InstantTypeError: When ``value`` is None. - - Examples: - >>> instant(datetime.date(2021, 9, 16)) - Instant((2021, 9, 16)) - - >>> instant(Instant((2021, 9, 16))) - Instant((2021, 9, 16)) - - >>> instant("2021") - Instant((2021, 1, 1)) - - >>> instant(2021) - Instant((2021, 1, 1)) - - >>> instant((2021, 9)) - Instant((2021, 9, 1)) - - >>> start = Instant((2021, 9, 16)) - - >>> instant(Period((year, start, 1))) - Traceback (most recent call last): - InstantFormatError: 'year:2021-09' is not a valid instant. - - .. versionadded:: 39.0.0 - - """ - - isoformat: ISOFormat | None - - if isinstance(value, Instant): - return value - - if isinstance(value, datetime.date): - return Instant((value.year, value.month, value.day)) - - if isinstance(value, int): - isoformat = fromint(value) - - elif isinstance(value, str): - isoformat = fromstr(value) - - elif isinstance(value, (list, tuple)): - isoformat = fromseq(value) - - else: - raise InstantTypeError(value) - - if isoformat is None: - raise InstantFormatError(value) - - return Instant((isoformat.year, isoformat.month, isoformat.day)) - - -def period(value: Any) -> Period: - """Build a new period, aka a triple (unit, start_instant, size). - - Args: - value: A ``period-like`` object. - - Returns: - A period. - - Raises: - PeriodFormatError: When arguments are invalid, like "2021-32-13". - PeriodTypeError: When ``value`` is not a ``period-like`` object. - - Examples: - >>> period(Period((year, Instant((2021, 1, 1)), 1))) - Period((year, Instant((2021, 1, 1)), 1)) - - >>> period(Instant((2021, 1, 1))) - Period((day, Instant((2021, 1, 1)), 1)) - - >>> period(eternity) - Period((eternity, Instant((1, 1, 1)), 1)) - - >>> period(2021) - Period((year, Instant((2021, 1, 1)), 1)) - - >>> period("2014") - Period((year, Instant((2014, 1, 1)), 1)) - - >>> period("year:2014") - Period((year, Instant((2014, 1, 1)), 1)) - - >>> period("month:2014-02") - Period((month, Instant((2014, 2, 1)), 1)) - - >>> period("year:2014-02") - Period((year, Instant((2014, 2, 1)), 1)) - - >>> period("day:2014-02-02") - Period((day, Instant((2014, 2, 2)), 1)) - - >>> period("day:2014-02-02:3") - Period((day, Instant((2014, 2, 2)), 3)) - - """ - - if value in {eternity, eternity.name, eternity.name.lower()}: - return Period((eternity, instant(datetime.date.min), 1)) - - if value is None or isinstance(value, DateUnit): - raise PeriodTypeError(value) - - if isinstance(value, Period): - return value - - if isinstance(value, Instant): - return Period((day, value, 1)) - - if isinstance(value, int): - return Period((year, Instant((value, 1, 1)), 1)) - - if not isinstance(value, str): - raise PeriodFormatError(value) - - # Try to parse as a complex period - isoformat = fromcomplex(value) - - if isoformat is None: - raise PeriodFormatError(value) - - unit = DateUnit(isoformat.unit) - - return Period((unit, instant(isoformat[:3]), isoformat.size)) diff --git a/openfisca_core/periods/_date_unit.py b/openfisca_core/periods/_dates.py similarity index 86% rename from openfisca_core/periods/_date_unit.py rename to openfisca_core/periods/_dates.py index c5fc27ea38..7b95daf909 100644 --- a/openfisca_core/periods/_date_unit.py +++ b/openfisca_core/periods/_dates.py @@ -1,8 +1,10 @@ from __future__ import annotations +from typing import NamedTuple + import enum -from ._exceptions import DateUnitValueError +from ._errors import DateUnitValueError class DateUnitMeta(enum.EnumMeta): @@ -140,3 +142,22 @@ def plural(self) -> str: return str(self) + "s" raise DateUnitValueError(self) + + +class ISOFormat(NamedTuple): + """A tuple representing a date in ISO format""" + + #: The year of the parsed period. + year: int + + #: The month of the parsed period. + month: int + + #: The month of the parsed period. + day: int + + #: The unit of the parsed period, in binary. + unit: int + + #: The size of the parsed instant or period. + size: int diff --git a/openfisca_core/periods/_exceptions.py b/openfisca_core/periods/_errors.py similarity index 100% rename from openfisca_core/periods/_exceptions.py rename to openfisca_core/periods/_errors.py diff --git a/openfisca_core/periods/_helpers.py b/openfisca_core/periods/_helpers.py new file mode 100644 index 0000000000..614dcae81a --- /dev/null +++ b/openfisca_core/periods/_helpers.py @@ -0,0 +1,395 @@ +from __future__ import annotations + +from typing import Any, Sequence + +import datetime + +import pendulum +from pendulum.datetime import Date +from pendulum.parsing import ParserError + +from ._dates import DateUnit, ISOFormat +from ._errors import ( + InstantFormatError, + InstantTypeError, + PeriodFormatError, + PeriodTypeError, + ) +from ._instants import Instant +from ._periods import Period + +day, _month, year, eternity = tuple(DateUnit) + + +def build_instant(value: Any) -> Instant: + """Build a new instant, aka a triple of integers (year, month, day). + + Args: + value: An ``instant-like`` object. + + Returns: + An Instant. + + Raises: + InstantFormatError: When ``value`` is invalid, like "2021-32-13". + InstantTypeError: When ``value`` is None. + + Examples: + >>> build_instant(datetime.date(2021, 9, 16)) + Instant((2021, 9, 16)) + + >>> build_instant(Instant((2021, 9, 16))) + 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)) + + >>> start = Instant((2021, 9, 16)) + + >>> build_instant(Period((year, start, 1))) + Traceback (most recent call last): + InstantFormatError: 'year:2021-09' is not a valid instant. + + .. versionadded:: 39.0.0 + + """ + + isoformat: ISOFormat | None + + if isinstance(value, Instant): + return value + + if isinstance(value, datetime.date): + return Instant((value.year, value.month, value.day)) + + if isinstance(value, int): + isoformat = parse_int(value) + + elif isinstance(value, str): + isoformat = parse_instant_str(value) + + elif isinstance(value, (list, tuple)): + isoformat = parse_seq(value) + + else: + raise InstantTypeError(value) + + if isoformat is None: + raise InstantFormatError(value) + + return Instant((isoformat.year, isoformat.month, isoformat.day)) + + +def build_period(value: Any) -> Period: + """Build a new period, aka a triple (unit, start_instant, size). + + Args: + value: A ``period-like`` object. + + Returns: + A period. + + Raises: + PeriodFormatError: When arguments are invalid, like "2021-32-13". + PeriodTypeError: When ``value`` is not a ``period-like`` object. + + Examples: + >>> build_period(Period((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(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 value in {eternity, eternity.name, eternity.name.lower()}: + return Period((eternity, build_instant(datetime.date.min), 1)) + + if value is None or isinstance(value, DateUnit): + raise PeriodTypeError(value) + + if isinstance(value, Period): + return value + + if isinstance(value, Instant): + return Period((day, value, 1)) + + if isinstance(value, int): + return Period((year, Instant((value, 1, 1)), 1)) + + if not isinstance(value, str): + raise PeriodFormatError(value) + + # Try to parse as a complex period + isoformat = parse_period_str(value) + + if isoformat is None: + raise PeriodFormatError(value) + + unit = DateUnit(isoformat.unit) + + return Period((unit, build_instant(isoformat[:3]), isoformat.size)) + + +def parse_int(value: int) -> ISOFormat | None: + """Parse an int respecting the ISO format. + + Args: + value: The integer to parse. + + Returns: + An ISOFormat object if ``value`` is valid. + None otherwise. + + Examples: + >>> parse_int(1) + ISOFormat(year=1, month=1, day=1, unit=4, size=1) + + >>> parse_int(2023) + ISOFormat(year=2023, month=1, day=1, unit=4, size=1) + + >>> parse_int(-1) + + >>> parse_int("2023") + + >>> parse_int(20231231) + + .. versionadded:: 39.0.0 + + """ + + if not isinstance(value, int): + return None + + if not 1 <= len(str(value)) <= 4: + return None + + try: + if not 1 <= int(str(value)[:4]) < 10000: + return None + + except ValueError: + return None + + return ISOFormat(value, 1, 1, 4, 1) + + +def parse_seq(value: Sequence[int]) -> ISOFormat | None: + """Parse a sequence of ints respecting the ISO format. + + Args: + value: A sequence of ints such as [2012, 3, 13]. + + Returns: + An ISOFormat object if ``value`` is valid. + None if ``value`` is not valid. + + Examples: + >>> parse_seq([2022]) + ISOFormat(year=2022, month=1, day=1, unit=4, size=1) + + >>> parse_seq([2022, 1]) + ISOFormat(year=2022, month=1, day=1, unit=2, size=1) + + >>> parse_seq([2022, 1, 1]) + ISOFormat(year=2022, month=1, day=1, unit=1, size=1) + + >>> parse_seq([-2022, 1, 1]) + + >>> parse_seq([2022, 13, 1]) + + >>> parse_seq([2022, 1, 32]) + + .. versionadded:: 39.0.0 + + """ + + if not isinstance(value, (list, tuple)): + return None + + if not value: + return None + + if not 1 <= len(value) <= 3: + return None + + if not all(isinstance(unit, int) for unit in value): + return None + + if not all(unit == abs(unit) for unit in value): + return None + + # We get the shape of the string (e.g. "2012-02" = 2) + shape = len(value) + + # We get the unit from the shape (e.g. 2 = "month") + unit = tuple(DateUnit)[3 - shape] + + while len(value) < 3: + value = (*value, 1) + + try: + # We parse the date + date = pendulum.date(*value) + + except ValueError: + return None + + if not isinstance(date, Date): + return None + + # We build the corresponding ISOFormat object + return ISOFormat(date.year, date.month, date.day, unit.value, 1) + + +def parse_instant_str(value: str) -> ISOFormat | None: + """Parse strings respecting the ISO format. + + Args: + value: A string such as such as "2012" or "2015-03". + + Returns: + An ISOFormat object if ``value`` is valid. + None if ``value`` is not valid. + + Examples: + >>> parse_instant_str("2022") + ISOFormat(year=2022, month=1, day=1, unit=4, size=1) + + >>> parse_instant_str("2022-02") + ISOFormat(year=2022, month=2, day=1, unit=2, size=1) + + >>> parse_instant_str("2022-02-13") + ISOFormat(year=2022, month=2, day=13, unit=1, size=1) + + >>> parse_instant_str(1000) + + >>> parse_instant_str("ETERNITY") + + .. versionadded:: 39.0.0 + + """ + + if not isinstance(value, str): + return None + + if not value: + return None + + # If it is a complex value, next! + if len(value.split(":")) != 1: + return None + + # If it's negative period, next! + if value[0] == "-": + return None + + try: + # We parse the date + date = pendulum.parse(value, exact = True, strict = True) + + except ParserError: + return None + + if not isinstance(date, Date): + return None + + # We get the shape of the string (e.g. "2012-02" = 2) + shape = len(value.split("-")) + + # We get the unit from the shape (e.g. 2 = "month") + unit = tuple(DateUnit)[3 - shape] + + # We build the corresponding ISOFormat object + return ISOFormat(date.year, date.month, date.day, unit.value, 1) + + +def parse_period_str(value: str) -> ISOFormat | None: + """Parse complex strings representing periods. + + Args: + value: A string such as such as "year:2012" or "month:2015-03:12". + + Returns: + An ISOFormat object if ``value`` is valid. + None if ``value`` is not valid. + + Examples: + >>> parse_period_str("year:2022") + ISOFormat(year=2022, month=1, day=1, unit=4, size=1) + + >>> parse_period_str("month:2022-02") + ISOFormat(year=2022, month=2, day=1, unit=2, size=1) + + >>> parse_period_str("day:2022-02-13:15") + ISOFormat(year=2022, month=2, day=13, unit=1, size=15) + + >>> parse_period_str("2022:3") + + >>> parse_period_str("ETERNITY") + + .. versionadded:: 39.0.0 + + """ + + if not isinstance(value, str): + return None + + if not value: + return None + + # If it is not a complex value, delegate! + if len(value.split(":")) == 1: + return parse_instant_str(value) + + first, second, *rest = value.split(":") + unit = DateUnit.__members__.get(first.upper()) + date = parse_instant_str(second) + + # If it is an invalid unit, next! + if unit is None: + return None + + # If it is an invalid date, next! + if date is None: + return None + + # If it has no size, we'll assume ``1`` + if not rest: + size = 1 + + else: + size = int(rest[0]) + + # We build the corresponding ISOFormat object + return ISOFormat(date.year, date.month, date.day, unit.value, size) diff --git a/openfisca_core/periods/_instant.py b/openfisca_core/periods/_instants.py similarity index 86% rename from openfisca_core/periods/_instant.py rename to openfisca_core/periods/_instants.py index bb4d840b7f..f82a1e8ddb 100644 --- a/openfisca_core/periods/_instant.py +++ b/openfisca_core/periods/_instants.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Tuple +from typing import Callable, Tuple import calendar import functools @@ -8,9 +8,8 @@ import pendulum from pendulum.datetime import Date -from ._date_unit import DateUnit -from ._exceptions import DateUnitValueError, OffsetTypeError -from ._utils import add +from ._dates import DateUnit +from ._errors import DateUnitValueError, OffsetTypeError DAY, MONTH, YEAR, _ = tuple(DateUnit) @@ -143,6 +142,31 @@ def date(self) -> Date: return pendulum.date(*self) + def add(self, unit: str, count: int) -> Date: + """Add ``count`` ``unit``s to a ``date``. + + Args: + unit: The unit to add. + count: The number of units to add. + + Returns: + A new Date. + + Examples: + >>> instant = Instant((2021, 10, 1)) + >>> instant.add("months", 6) + Date(2022, 4, 1) + + .. versionadded:: 39.0.0 + + """ + + fun: Callable[..., Date] = self.date().add + + new: Date = fun(**{unit: count}) + + return new + def offset(self, offset: str | int, unit: DateUnit) -> Instant: """Increments/decrements the given instant with offset units. @@ -199,6 +223,6 @@ def offset(self, offset: str | int, unit: DateUnit) -> Instant: if not isinstance(offset, int): raise OffsetTypeError(offset) - date = add(self.date(), unit.plural, offset) + date = self.add(unit.plural, offset) return type(self)((date.year, date.month, date.day)) diff --git a/openfisca_core/periods/_iso_format.py b/openfisca_core/periods/_iso_format.py deleted file mode 100644 index 36be7638b7..0000000000 --- a/openfisca_core/periods/_iso_format.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -from typing import NamedTuple - - -class ISOFormat(NamedTuple): - """An implementation of the `parse` protocol.""" - - #: The year of the parsed period. - year: int - - #: The month of the parsed period. - month: int - - #: The month of the parsed period. - day: int - - #: The unit of the parsed period, in binary. - unit: int - - #: The size of the parsed instant or period. - size: int diff --git a/openfisca_core/periods/_parsers.py b/openfisca_core/periods/_parsers.py deleted file mode 100644 index e2e6cd013a..0000000000 --- a/openfisca_core/periods/_parsers.py +++ /dev/null @@ -1,244 +0,0 @@ -from __future__ import annotations - -from typing import Sequence - -import pendulum -from pendulum.datetime import Date -from pendulum.parsing import ParserError - -from ._date_unit import DateUnit -from ._iso_format import ISOFormat - - -def fromint(value: int) -> ISOFormat | None: - """Parse an int respecting the ISO format. - - Args: - value: The integer to parse. - - Returns: - An ISOFormat object if ``value`` is valid. - None otherwise. - - Examples: - >>> fromint(1) - ISOFormat(year=1, month=1, day=1, unit=4, size=1) - - >>> fromint(2023) - ISOFormat(year=2023, month=1, day=1, unit=4, size=1) - - >>> fromint(-1) - - >>> fromint("2023") - - >>> fromint(20231231) - - .. versionadded:: 39.0.0 - - """ - - if not isinstance(value, int): - return None - - if not 1 <= len(str(value)) <= 4: - return None - - try: - if not 1 <= int(str(value)[:4]) < 10000: - return None - - except ValueError: - return None - - return ISOFormat(value, 1, 1, 4, 1) - - -def fromseq(value: Sequence[int]) -> ISOFormat | None: - """Parse a sequence of ints respecting the ISO format. - - Args: - value: A sequence of ints such as [2012, 3, 13]. - - Returns: - An ISOFormat object if ``value`` is valid. - None if ``value`` is not valid. - - Examples: - >>> fromseq([2022]) - ISOFormat(year=2022, month=1, day=1, unit=4, size=1) - - >>> fromseq([2022, 1]) - ISOFormat(year=2022, month=1, day=1, unit=2, size=1) - - >>> fromseq([2022, 1, 1]) - ISOFormat(year=2022, month=1, day=1, unit=1, size=1) - - >>> fromseq([-2022, 1, 1]) - - >>> fromseq([2022, 13, 1]) - - >>> fromseq([2022, 1, 32]) - - .. versionadded:: 39.0.0 - - """ - - if not isinstance(value, (list, tuple)): - return None - - if not value: - return None - - if not 1 <= len(value) <= 3: - return None - - if not all(isinstance(unit, int) for unit in value): - return None - - if not all(unit == abs(unit) for unit in value): - return None - - # We get the shape of the string (e.g. "2012-02" = 2) - shape = len(value) - - # We get the unit from the shape (e.g. 2 = "month") - unit = tuple(DateUnit)[3 - shape] - - while len(value) < 3: - value = (*value, 1) - - try: - # We parse the date - date = pendulum.date(*value) - - except ValueError: - return None - - if not isinstance(date, Date): - return None - - # We build the corresponding ISOFormat object - return ISOFormat(date.year, date.month, date.day, unit.value, 1) - - -def fromstr(value: str) -> ISOFormat | None: - """Parse strings respecting the ISO format. - - Args: - value: A string such as such as "2012" or "2015-03". - - Returns: - An ISOFormat object if ``value`` is valid. - None if ``value`` is not valid. - - Examples: - >>> fromstr("2022") - ISOFormat(year=2022, month=1, day=1, unit=4, size=1) - - >>> fromstr("2022-02") - ISOFormat(year=2022, month=2, day=1, unit=2, size=1) - - >>> fromstr("2022-02-13") - ISOFormat(year=2022, month=2, day=13, unit=1, size=1) - - >>> fromstr(1000) - - >>> fromstr("ETERNITY") - - .. versionadded:: 39.0.0 - - """ - - if not isinstance(value, str): - return None - - if not value: - return None - - # If it is a complex value, next! - if len(value.split(":")) != 1: - return None - - # If it's negative period, next! - if value[0] == "-": - return None - - try: - # We parse the date - date = pendulum.parse(value, exact = True, strict = True) - - except ParserError: - return None - - if not isinstance(date, Date): - return None - - # We get the shape of the string (e.g. "2012-02" = 2) - shape = len(value.split("-")) - - # We get the unit from the shape (e.g. 2 = "month") - unit = tuple(DateUnit)[3 - shape] - - # We build the corresponding ISOFormat object - return ISOFormat(date.year, date.month, date.day, unit.value, 1) - - -def fromcomplex(value: str) -> ISOFormat | None: - """Parse complex strings representing periods. - - Args: - value: A string such as such as "year:2012" or "month:2015-03:12". - - Returns: - An ISOFormat object if ``value`` is valid. - None if ``value`` is not valid. - - Examples: - >>> fromcomplex("year:2022") - ISOFormat(year=2022, month=1, day=1, unit=4, size=1) - - >>> fromcomplex("month:2022-02") - ISOFormat(year=2022, month=2, day=1, unit=2, size=1) - - >>> fromcomplex("day:2022-02-13:15") - ISOFormat(year=2022, month=2, day=13, unit=1, size=15) - - >>> fromcomplex("2022:3") - - >>> fromcomplex("ETERNITY") - - .. versionadded:: 39.0.0 - - """ - - if not isinstance(value, str): - return None - - if not value: - return None - - # If it is not a complex value, delegate! - if len(value.split(":")) == 1: - return fromstr(value) - - first, second, *rest = value.split(":") - unit = DateUnit.__members__.get(first.upper()) - date = fromstr(second) - - # If it is an invalid unit, next! - if unit is None: - return None - - # If it is an invalid date, next! - if date is None: - return None - - # If it has no size, we'll assume ``1`` - if not rest: - size = 1 - - else: - size = int(rest[0]) - - # We build the corresponding ISOFormat object - return ISOFormat(date.year, date.month, date.day, unit.value, size) diff --git a/openfisca_core/periods/_period.py b/openfisca_core/periods/_periods.py similarity index 99% rename from openfisca_core/periods/_period.py rename to openfisca_core/periods/_periods.py index fb350049b5..3e74b8fbf1 100644 --- a/openfisca_core/periods/_period.py +++ b/openfisca_core/periods/_periods.py @@ -4,9 +4,9 @@ import datetime -from ._date_unit import DateUnit -from ._exceptions import DateUnitValueError -from ._instant import Instant +from ._dates import DateUnit +from ._errors import DateUnitValueError +from ._instants import Instant DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) diff --git a/openfisca_core/periods/_utils.py b/openfisca_core/periods/_utils.py deleted file mode 100644 index d9d2206017..0000000000 --- a/openfisca_core/periods/_utils.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import annotations - -from typing import Callable - -from pendulum.datetime import Date - - -def add(date: Date, unit: str, count: int) -> Date: - """Add ``count`` ``unit``s to a ``date``. - - Args: - date: The date to add to. - unit: The unit to add. - count: The number of units to add. - - Returns: - A new Date. - - Examples: - >>> add(Date(2022, 1, 1), "years", 1) - Date(2023, 1, 1) - - .. versionadded:: 39.0.0 - - """ - - fun: Callable[..., Date] = date.add - - new: Date = fun(**{unit: count}) - - return new diff --git a/openfisca_core/periods/tests/test_parsers.py b/openfisca_core/periods/tests/test_parsers.py index b6f462d611..dfd108da3c 100644 --- a/openfisca_core/periods/tests/test_parsers.py +++ b/openfisca_core/periods/tests/test_parsers.py @@ -1,6 +1,8 @@ import pytest -from openfisca_core.periods import DateUnit, isoformat +from openfisca_core import periods +from openfisca_core.periods import DateUnit +from openfisca_core.periods import _helpers as parsers year = DateUnit.YEAR @@ -44,7 +46,7 @@ def test_parse_iso_format_from_int(arg, expected): """Returns an ``ISOFormat`` when given a valid ISO format int.""" - assert isoformat.fromint(arg) == expected + assert parsers.parse_int(arg) == expected @pytest.mark.parametrize("arg, expected", [ @@ -86,7 +88,7 @@ def test_parse_iso_format_from_int(arg, expected): def test_parse_iso_format_from_seq(arg, expected): """Returns an ``ISOFormat`` when given a valid ISO format sequence.""" - assert isoformat.fromseq(arg) == expected + assert parsers.parse_seq(arg) == expected @pytest.mark.parametrize("arg, expected", [ @@ -101,7 +103,7 @@ def test_parse_iso_format_from_seq(arg, expected): ["999", None], ["eternity", None], ["first-of", None], - ["year:2021:7", None], + ["year:2021:7", (2021, 1, 1, 4, 7)], [(1, 1), None], [(1, 1, 1), None], [(1, 1, 1, 1), None], @@ -125,10 +127,10 @@ def test_parse_iso_format_from_seq(arg, expected): [{1, 1}, None], [{1, }, None], ]) -def test_parse_iso_format_from_str(arg, expected): - """Returns an ``ISOFormat`` when given a valid ISO format string.""" +def test_parse_iso_format_from_complex_str(arg, expected): + """Returns an ``ISOFormat`` when given a valid complex period.""" - assert isoformat.fromstr(arg) == expected + assert parsers.parse_period_str(arg) == expected @pytest.mark.parametrize("arg, expected", [ @@ -143,7 +145,7 @@ def test_parse_iso_format_from_str(arg, expected): ["999", None], ["eternity", None], ["first-of", None], - ["year:2021:7", (2021, 1, 1, 4, 7)], + ["year:2021:7", None], [(1, 1), None], [(1, 1, 1), None], [(1, 1, 1, 1), None], @@ -167,7 +169,7 @@ def test_parse_iso_format_from_str(arg, expected): [{1, 1}, None], [{1, }, None], ]) -def test_parse_iso_format_from_complex_str(arg, expected): - """Returns an ``ISOFormat`` when given a valid complex period.""" +def test_parse_iso_format_from_str(arg, expected): + """Returns an ``ISOFormat`` when given a valid ISO format string.""" - assert isoformat.fromcomplex(arg) == expected + assert periods.parse(arg) == expected diff --git a/setup.cfg b/setup.cfg index 8585166901..9e0140eed0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,6 @@ 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 From 23facd0f9ed4b65c069bea9b74b58aaafb7b5470 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Tue, 20 Dec 2022 00:28:07 +0100 Subject: [PATCH 85/93] Keep file names --- openfisca_core/periods/__init__.py | 10 +++++----- openfisca_core/periods/{_helpers.py => helpers.py} | 4 ++-- openfisca_core/periods/{_instants.py => instant_.py} | 0 openfisca_core/periods/{_periods.py => period_.py} | 2 +- openfisca_core/periods/tests/test_parsers.py | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) rename openfisca_core/periods/{_helpers.py => helpers.py} (99%) rename openfisca_core/periods/{_instants.py => instant_.py} (100%) rename openfisca_core/periods/{_periods.py => period_.py} (99%) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index ce8e2cb5a7..1d555fe847 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -28,11 +28,11 @@ """ from ._dates import DateUnit -from ._helpers import build_instant as instant -from ._helpers import build_period as period -from ._helpers import parse_instant_str as parse -from ._instants import Instant -from ._periods import Period +from .helpers import build_instant as instant +from .helpers import build_period as period +from .helpers import parse_instant_str as parse +from .instant_ import Instant +from .period_ import Period DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) diff --git a/openfisca_core/periods/_helpers.py b/openfisca_core/periods/helpers.py similarity index 99% rename from openfisca_core/periods/_helpers.py rename to openfisca_core/periods/helpers.py index 614dcae81a..134dc79446 100644 --- a/openfisca_core/periods/_helpers.py +++ b/openfisca_core/periods/helpers.py @@ -15,8 +15,8 @@ PeriodFormatError, PeriodTypeError, ) -from ._instants import Instant -from ._periods import Period +from .instant_ import Instant +from .period_ import Period day, _month, year, eternity = tuple(DateUnit) diff --git a/openfisca_core/periods/_instants.py b/openfisca_core/periods/instant_.py similarity index 100% rename from openfisca_core/periods/_instants.py rename to openfisca_core/periods/instant_.py diff --git a/openfisca_core/periods/_periods.py b/openfisca_core/periods/period_.py similarity index 99% rename from openfisca_core/periods/_periods.py rename to openfisca_core/periods/period_.py index 3e74b8fbf1..1bdd1eb035 100644 --- a/openfisca_core/periods/_periods.py +++ b/openfisca_core/periods/period_.py @@ -6,7 +6,7 @@ from ._dates import DateUnit from ._errors import DateUnitValueError -from ._instants import Instant +from .instant_ import Instant DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) diff --git a/openfisca_core/periods/tests/test_parsers.py b/openfisca_core/periods/tests/test_parsers.py index dfd108da3c..87f8f3c695 100644 --- a/openfisca_core/periods/tests/test_parsers.py +++ b/openfisca_core/periods/tests/test_parsers.py @@ -2,7 +2,7 @@ from openfisca_core import periods from openfisca_core.periods import DateUnit -from openfisca_core.periods import _helpers as parsers +from openfisca_core.periods import helpers as parsers year = DateUnit.YEAR From d82f78fb0579da2fd09bea93aab24d5ed1b9fc29 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 22 Dec 2022 19:28:54 +0100 Subject: [PATCH 86/93] Add the offsetable protocol --- CHANGELOG.md | 2 +- openfisca_core/periods/__init__.py | 4 +- openfisca_core/periods/period_.py | 79 +++++++++++++++---- openfisca_core/periods/typing.py | 56 +++++++++++++ openfisca_core/simulations/simulation.py | 6 +- .../taxbenefitsystems/tax_benefit_system.py | 5 +- openfisca_core/variables/helpers.py | 2 +- tests/core/test_countries.py | 4 +- 8 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 openfisca_core/periods/typing.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4460b5ca20..593e803b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ - This has been simplified to allow users to build their own: - `period.ago(unit: DateUnit, size: int = 1) -> Period`. - `period.last(unit: DateUnit, size: int = 1) -> Period`. - - `period.this(unit: DateUnit) -> Period`. + - `period.first(unit: DateUnit) -> Period`. - Rationalise date units. - Before, usage of "month", YEAR, and so on was fairly inconsistent, and providing a perfect hotbed for bugs to breed. diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 1d555fe847..c293f51e11 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -38,5 +38,5 @@ # Deprecated -setattr(Period, "this_year", property(lambda self: self.this(YEAR))) # noqa: B010 -setattr(Period, "first_month", property(lambda self: self.this(MONTH))) # noqa: B010 +setattr(Period, "this_year", property(lambda self: self.first(YEAR))) # noqa: B010 +setattr(Period, "first_month", property(lambda self: self.first(MONTH))) # noqa: B010 diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 1bdd1eb035..73d3150abf 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -6,12 +6,12 @@ from ._dates import DateUnit from ._errors import DateUnitValueError -from .instant_ import Instant +from .typing import Offsetable DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) -class Period(Tuple[DateUnit, Instant, int]): +class Period(Tuple[DateUnit, Offsetable[int, int, int], int]): """Toolbox to handle date intervals. A ``Period`` is a triple (``unit``, ``start``, ``size``). @@ -26,6 +26,8 @@ class Period(Tuple[DateUnit, Instant, int]): The ``unit``, ``start``, and ``size``, accordingly. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2021, 9, 1)) >>> period = Period((YEAR, start, 3)) @@ -84,6 +86,8 @@ def __str__(self) -> str: A string representation of the period. Examples: + >>> from openfisca_core.periods import Instant + >>> jan = Instant((2021, 1, 1)) >>> feb = jan.offset(1, MONTH) @@ -107,12 +111,16 @@ def __str__(self) -> str: """ - unit, start, size = self + size = self.size + start = self.start + unit = self.unit - if unit == ETERNITY: - return unit.name + day = start.day + month = start.month + year = start.year - year, month, day = start + if unit == ETERNITY: + return str(unit.name) # 1 year long period if unit == MONTH and size == 12 or unit == YEAR and size == 1: @@ -152,9 +160,12 @@ def __contains__(self, other: object) -> bool: True if ``other`` is contained, otherwise False. Example: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2021, 1, 1)) >>> period = Period((YEAR, start, 1)) >>> sub_period = Period((MONTH, start, 3)) + >>> sub_period in period True @@ -173,8 +184,11 @@ def unit(self) -> DateUnit: A DateUnit. Example: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) + >>> period.unit year @@ -183,15 +197,18 @@ def unit(self) -> DateUnit: return self[0] @property - def start(self) -> Instant: + def start(self) -> Offsetable[int, int, int]: """The ``Instant`` at which the ``Period`` starts. Returns: An Instant. Example: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) + >>> period.start Instant((2021, 10, 1)) @@ -207,8 +224,11 @@ def size(self) -> int: An int. Example: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) + >>> period.size 3 @@ -217,13 +237,15 @@ def size(self) -> int: return self[2] @property - def stop(self) -> Instant: + def stop(self) -> Offsetable[int, int, int]: """Last day of the ``Period`` as an ``Instant``. Returns: An Instant. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2012, 2, 29)) >>> Period((YEAR, start, 2)).stop @@ -254,6 +276,8 @@ def date(self) -> datetime.date: ValueError: If the period's size is greater than 1. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 1)) @@ -288,6 +312,8 @@ def count(self, unit: DateUnit) -> int: ValueError: If the period's unit is not a day, a month or a year. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2021, 10, 1)) >>> period = Period((YEAR, start, 3)) @@ -335,7 +361,7 @@ def count(self, unit: DateUnit) -> int: f"{str(self.unit)}." ) - def this(self, unit: DateUnit) -> Period: + def first(self, unit: DateUnit) -> Period: """A new month ``Period`` starting at the first of ``unit``. Args: @@ -345,19 +371,22 @@ def this(self, unit: DateUnit) -> Period: A Period. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) - >>> period.this(DAY) + >>> period.first(DAY) Period((day, Instant((2023, 1, 1)), 1)) - >>> period.this(MONTH) + >>> period.first(MONTH) Period((month, Instant((2023, 1, 1)), 1)) - >>> period.this(YEAR) + >>> period.first(YEAR) Period((year, Instant((2023, 1, 1)), 1)) + .. versionadded:: 39.0.0 """ @@ -374,6 +403,8 @@ def come(self, unit: DateUnit, size: int = 1) -> Period: A Period. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -396,9 +427,11 @@ def come(self, unit: DateUnit, size: int = 1) -> Period: >>> period.come(YEAR, 1) Period((year, Instant((2024, 1, 1)), 1)) + .. versionadded:: 39.0.0 + """ - return type(self)((unit, self.this(unit).start, 1)).offset(size) + return type(self)((unit, self.first(unit).start, 1)).offset(size) def ago(self, unit: DateUnit, size: int = 1) -> Period: """``size`` ``unit``s ago from ``Period.start``. @@ -411,6 +444,8 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: A Period. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -433,6 +468,8 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: >>> period.ago(YEAR, 1) Period((year, Instant((2022, 1, 1)), 1)) + .. versionadded:: 39.0.0 + """ return self.come(unit, -size) @@ -448,6 +485,8 @@ def until(self, unit: DateUnit, size: int = 1) -> Period: A Period. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -470,9 +509,11 @@ def until(self, unit: DateUnit, size: int = 1) -> Period: >>> period.until(YEAR, 1) Period((year, Instant((2023, 1, 1)), 1)) + .. versionadded:: 39.0.0 + """ - return type(self)((unit, self.this(unit).start, size)) + return type(self)((unit, self.first(unit).start, size)) def last(self, unit: DateUnit, size: int = 1) -> Period: """Last ``size`` ``unit``s from ``Period.start``. @@ -485,6 +526,8 @@ def last(self, unit: DateUnit, size: int = 1) -> Period: A Period. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2023, 1, 1)) >>> period = Period((YEAR, start, 3)) @@ -507,6 +550,8 @@ def last(self, unit: DateUnit, size: int = 1) -> Period: >>> period.last(YEAR, 1) Period((year, Instant((2022, 1, 1)), 1)) + .. versionadded:: 39.0.0 + """ return type(self)((unit, self.ago(unit, size).start, size)) @@ -522,6 +567,8 @@ def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: Period: A new one. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2014, 2, 3)) >>> Period((DAY, start, 1)).offset("first-of", MONTH) @@ -561,6 +608,8 @@ def subperiods(self, unit: DateUnit) -> Sequence[Period]: ValueError: If the period's unit is smaller than the given unit. Examples: + >>> from openfisca_core.periods import Instant + >>> start = Instant((2021, 1, 1)) >>> period = Period((YEAR, start, 1)) @@ -583,6 +632,6 @@ def subperiods(self, unit: DateUnit) -> Sequence[Period]: raise DateUnitValueError(unit) return [ - self.this(unit).offset(offset, unit) + self.first(unit).offset(offset, unit) for offset in range(self.count(unit)) ] diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py new file mode 100644 index 0000000000..d3098ab70a --- /dev/null +++ b/openfisca_core/periods/typing.py @@ -0,0 +1,56 @@ +# pylint: disable=missing-class-docstring, missing-function-docstring + +from __future__ import annotations + +from typing import Any, Tuple, TypeVar +from typing_extensions import Protocol + +import abc + +from pendulum.datetime import Date + +Self = TypeVar("Self") +T = TypeVar("T", covariant = True) +U = TypeVar("U", covariant = True) +V = TypeVar("V", covariant = True) + + +class Offsetable(Protocol[T, U, V]): + @abc.abstractmethod + def __init__(self, values: Tuple[T, U, V]) -> None: + ... + + @abc.abstractmethod + def __le__(self, other: Any) -> bool: + ... + + @abc.abstractmethod + def __ge__(self, other: Any) -> bool: + ... + + @property + @abc.abstractmethod + def year(self) -> int: + ... + + @property + @abc.abstractmethod + def month(self) -> int: + ... + + @property + @abc.abstractmethod + def day(self) -> int: + ... + + @abc.abstractmethod + def add(self, unit: str, count: int) -> Date: + ... + + @abc.abstractmethod + def date(self) -> Date: + ... + + @abc.abstractmethod + def offset(self: Self, offset: Any, unit: Any) -> Self: + ... diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 8b086a0a2e..26b46b883d 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -179,7 +179,7 @@ def calculate_add(self, variable_name: str, period): # Check that the requested period matches definition_period if variable.definition_period > 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( + 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.first(YEAR)'.".format( variable.name, period, variable.definition_period @@ -216,7 +216,7 @@ def calculate_divide(self, variable_name: str, period): raise ValueError("DIVIDE option can only be used for a one-year or a one-month requested period") if period.unit == periods.MONTH: - computation_period = period.this(periods.YEAR) + computation_period = period.first(periods.YEAR) return self.calculate(variable_name, period = computation_period) / 12. elif period.unit == periods.YEAR: return self.calculate(variable_name, period) @@ -283,7 +283,7 @@ def _check_period_consistency(self, period, variable): )) if variable.definition_period == periods.YEAR and period.unit != periods.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( + 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.first(YEAR)'.".format( variable.name, period )) diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 786773b273..d636224234 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -25,6 +25,7 @@ from openfisca_core.populations import GroupPopulation, Population from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable +from openfisca_core.periods.typing import Offsetable log = logging.getLogger(__name__) @@ -387,7 +388,7 @@ def _get_baseline_parameters_at_instant(self, instant): @functools.lru_cache() def get_parameters_at_instant( self, - instant: periods.Period | periods.Instant | str | int, + instant: periods.Period | Offsetable[int, int, int] | str | int, ) -> Optional[types.ParameterNodeAtInstant]: """Get the parameters of the legislation at a given instant @@ -399,6 +400,8 @@ def get_parameters_at_instant( """ + key: Offsetable[int, int, int] + if isinstance(instant, periods.Instant): key = instant diff --git a/openfisca_core/variables/helpers.py b/openfisca_core/variables/helpers.py index 931af68351..a9e9fc2fc9 100644 --- a/openfisca_core/variables/helpers.py +++ b/openfisca_core/variables/helpers.py @@ -19,7 +19,7 @@ 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 period not in annualization_period): - return population(variable.name, period.this(periods.YEAR).this(periods.MONTH)) + return population(variable.name, period.first(periods.YEAR).first(periods.MONTH)) if original_formula.__code__.co_argcount == 2: return original_formula(population, period) return original_formula(population, period, parameters) diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index dd0848b40a..74fd34f38a 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -51,7 +51,7 @@ def test_non_existing_variable(simulation): @pytest.mark.parametrize("simulation", [({}, PERIOD)], indirect = True) def test_calculate_variable_with_wrong_definition_period(simulation): - year = str(PERIOD.this(periods.YEAR)) + year = str(PERIOD.first(periods.YEAR)) with pytest.raises(ValueError) as error: simulation.calculate("basic_income", year) @@ -84,7 +84,7 @@ def test_divide_option_with_complex_period(simulation): def test_input_with_wrong_period(tax_benefit_system): - year = str(PERIOD.this(periods.YEAR)) + year = str(PERIOD.first(periods.YEAR)) variables = {"basic_income": {year: 12000}} simulation_builder = SimulationBuilder() simulation_builder.set_default_period(PERIOD) From aac33e13817db99fbd57310a55fc36f56e44fb8c Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 22 Dec 2022 20:46:37 +0100 Subject: [PATCH 87/93] Make Instant more anglo-saxpon friendly --- openfisca_core/holders/tests/test_helpers.py | 4 +- openfisca_core/periods/helpers.py | 44 +++---- openfisca_core/periods/instant_.py | 107 +++++----------- openfisca_core/periods/period_.py | 117 +++++++++--------- openfisca_core/periods/tests/test_builders.py | 74 +++++------ openfisca_core/periods/tests/test_instant.py | 22 ++-- openfisca_core/periods/tests/test_period.py | 112 ++++++++--------- openfisca_core/periods/typing.py | 2 +- openfisca_core/simulations/simulation.py | 2 +- .../taxbenefitsystems/tax_benefit_system.py | 2 +- 10 files changed, 218 insertions(+), 268 deletions(-) diff --git a/openfisca_core/holders/tests/test_helpers.py b/openfisca_core/holders/tests/test_helpers.py index 333e319a61..0847baf90f 100644 --- a/openfisca_core/holders/tests/test_helpers.py +++ b/openfisca_core/holders/tests/test_helpers.py @@ -57,7 +57,7 @@ def test_set_input_dispatch_by_period( Income.definition_period = definition_unit income = Income() holder = Holder(income, population) - instant = periods.Instant((2022, 1, 1)) + instant = periods.Instant(2022, 1, 1) dispatch_period = periods.Period((dispatch_unit, instant, 3)) holders.set_input_dispatch_by_period(holder, dispatch_period, values) @@ -88,7 +88,7 @@ def test_set_input_divide_by_period( Income.definition_period = definition_unit income = Income() holder = Holder(income, population) - instant = periods.Instant((2022, 1, 1)) + instant = periods.Instant(2022, 1, 1) divide_period = periods.Period((divide_unit, instant, 3)) holders.set_input_divide_by_period(holder, divide_period, values) diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 134dc79446..37c76a3106 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -36,21 +36,21 @@ def build_instant(value: Any) -> Instant: Examples: >>> build_instant(datetime.date(2021, 9, 16)) - Instant((2021, 9, 16)) + Instant(year=2021, month=9, day=16) - >>> build_instant(Instant((2021, 9, 16))) - Instant((2021, 9, 16)) + >>> build_instant(Instant(2021, 9, 16)) + Instant(year=2021, month=9, day=16) >>> build_instant("2021") - Instant((2021, 1, 1)) + Instant(year=2021, month=1, day=1) >>> build_instant(2021) - Instant((2021, 1, 1)) + Instant(year=2021, month=1, day=1) >>> build_instant((2021, 9)) - Instant((2021, 9, 1)) + Instant(year=2021, month=9, day=1) - >>> start = Instant((2021, 9, 16)) + >>> start = Instant(2021, 9, 16) >>> build_instant(Period((year, start, 1))) Traceback (most recent call last): @@ -66,7 +66,7 @@ def build_instant(value: Any) -> Instant: return value if isinstance(value, datetime.date): - return Instant((value.year, value.month, value.day)) + return Instant(value.year, value.month, value.day) if isinstance(value, int): isoformat = parse_int(value) @@ -83,7 +83,7 @@ def build_instant(value: Any) -> Instant: if isoformat is None: raise InstantFormatError(value) - return Instant((isoformat.year, isoformat.month, isoformat.day)) + return Instant(isoformat.year, isoformat.month, isoformat.day) def build_period(value: Any) -> Period: @@ -100,35 +100,35 @@ def build_period(value: Any) -> Period: PeriodTypeError: When ``value`` is not a ``period-like`` object. Examples: - >>> build_period(Period((year, Instant((2021, 1, 1)), 1))) - Period((year, Instant((2021, 1, 1)), 1)) + >>> build_period(Period((year, Instant(2021, 1, 1), 1))) + Period((year, Instant(year=2021, month=1, day=1), 1)) - >>> build_period(Instant((2021, 1, 1))) - Period((day, Instant((2021, 1, 1)), 1)) + >>> build_period(Instant(2021, 1, 1)) + Period((day, Instant(year=2021, month=1, day=1), 1)) >>> build_period(eternity) - Period((eternity, Instant((1, 1, 1)), 1)) + Period((eternity, Instant(year=1, month=1, day=1), 1)) >>> build_period(2021) - Period((year, Instant((2021, 1, 1)), 1)) + Period((year, Instant(year=2021, month=1, day=1), 1)) >>> build_period("2014") - Period((year, Instant((2014, 1, 1)), 1)) + Period((year, Instant(year=2014, month=1, day=1), 1)) >>> build_period("year:2014") - Period((year, Instant((2014, 1, 1)), 1)) + Period((year, Instant(year=2014, month=1, day=1), 1)) >>> build_period("month:2014-02") - Period((month, Instant((2014, 2, 1)), 1)) + Period((month, Instant(year=2014, month=2, day=1), 1)) >>> build_period("year:2014-02") - Period((year, Instant((2014, 2, 1)), 1)) + Period((year, Instant(year=2014, month=2, day=1), 1)) >>> build_period("day:2014-02-02") - Period((day, Instant((2014, 2, 2)), 1)) + Period((day, Instant(year=2014, month=2, day=2), 1)) >>> build_period("day:2014-02-02:3") - Period((day, Instant((2014, 2, 2)), 3)) + Period((day, Instant(year=2014, month=2, day=2), 3)) """ @@ -145,7 +145,7 @@ def build_period(value: Any) -> Period: return Period((day, value, 1)) if isinstance(value, int): - return Period((year, Instant((value, 1, 1)), 1)) + return Period((year, Instant(value, 1, 1), 1)) if not isinstance(value, str): raise PeriodFormatError(value) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index f82a1e8ddb..784419f867 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Callable, Tuple +from typing import Callable, NamedTuple import calendar import functools @@ -14,7 +14,7 @@ DAY, MONTH, YEAR, _ = tuple(DateUnit) -class Instant(Tuple[int, int, int]): +class Instant(NamedTuple): """An instant in time (``year``, ``month``, ``day``). An ``Instant`` represents the most atomic and indivisible @@ -23,17 +23,13 @@ class Instant(Tuple[int, int, int]): Current implementation considers this unit to be a day, so ``instants`` can be thought of as "day dates". - Args: - (tuple(int, int, int)): - The ``year``, ``month``, and ``day``, accordingly. - Examples: - >>> instant = Instant((2021, 9, 13)) + >>> instant = Instant(2021, 9, 13) ``Instants`` are represented as a ``tuple`` containing the date units: >>> repr(instant) - 'Instant((2021, 9, 13))' + 'Instant(year=2021, month=9, day=13)' However, their user-friendly representation is as a date in the ISO format: @@ -44,8 +40,8 @@ class Instant(Tuple[int, int, int]): 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)} + >>> {instant: (2021, 9, 13)} + {Instant(year=2021, month=9, day=13): (2021, 9, 13)} All the rest of the ``tuple`` protocols are inherited as well: @@ -68,70 +64,25 @@ class Instant(Tuple[int, int, int]): """ - def __repr__(self) -> str: - return ( - f"{type(self).__name__}" - f"({super(type(self), self).__repr__()})" - ) + #: The year. + year: int + + #: The month. + month: int + + #: The day. + day: int @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) -> Date: """The date representation of the ``Instant``. Example: - >>> instant = Instant((2021, 10, 1)) + >>> instant = Instant(2021, 10, 1) >>> instant.date() Date(2021, 10, 1) @@ -153,7 +104,7 @@ def add(self, unit: str, count: int) -> Date: A new Date. Examples: - >>> instant = Instant((2021, 10, 1)) + >>> instant = Instant(2021, 10, 1) >>> instant.add("months", 6) Date(2022, 4, 1) @@ -182,17 +133,17 @@ def offset(self, offset: str | int, unit: DateUnit) -> Instant: OffsetTypeError: When ``offset`` is of type ``int``. Examples: - >>> Instant((2020, 12, 31)).offset("first-of", MONTH) - Instant((2020, 12, 1)) + >>> Instant(2020, 12, 31).offset("first-of", MONTH) + Instant(year=2020, month=12, day=1) - >>> Instant((2020, 1, 1)).offset("last-of", YEAR) - Instant((2020, 12, 31)) + >>> Instant(2020, 1, 1).offset("last-of", YEAR) + Instant(year=2020, month=12, day=31) - >>> Instant((2020, 1, 1)).offset(1, YEAR) - Instant((2021, 1, 1)) + >>> Instant(2020, 1, 1).offset(1, YEAR) + Instant(year=2021, month=1, day=1) - >>> Instant((2020, 1, 1)).offset(-3, DAY) - Instant((2019, 12, 29)) + >>> Instant(2020, 1, 1).offset(-3, DAY) + Instant(year=2019, month=12, day=29) """ @@ -208,21 +159,21 @@ def offset(self, offset: str | int, unit: DateUnit) -> Instant: return self if offset == "first-of" and unit == MONTH: - return type(self)((year, month, 1)) + return type(self)(year, month, 1) if offset == "first-of" and unit == YEAR: - return type(self)((year, 1, 1)) + return type(self)(year, 1, 1) if offset == "last-of" and unit == MONTH: day = calendar.monthrange(year, month)[1] - return type(self)((year, month, day)) + return type(self)(year, month, day) if offset == "last-of" and unit == YEAR: - return type(self)((year, 12, 31)) + return type(self)(year, 12, 31) if not isinstance(offset, int): raise OffsetTypeError(offset) date = self.add(unit.plural, offset) - return type(self)((date.year, date.month, date.day)) + return type(self)(date.year, date.month, date.day) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 73d3150abf..bbc523d5b1 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -28,14 +28,14 @@ class Period(Tuple[DateUnit, Offsetable[int, int, int], int]): Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2021, 9, 1)) + >>> start = Instant(2021, 9, 1) >>> period = Period((YEAR, start, 3)) ``Periods`` are represented as a ``tuple`` containing the ``unit``, an ``Instant`` and the ``size``: >>> repr(period) - 'Period((year, Instant((2021, 9, 1)), 3))' + 'Period((year, Instant(year=2021, month=9, day=1), 3))' Their user-friendly representation is as a date in the ISO format, prefixed with the ``unit`` and suffixed with its ``size``: @@ -46,9 +46,8 @@ class Period(Tuple[DateUnit, Offsetable[int, int, int], int]): 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... + >>> {period: (2021, 9, 13)} + {Period((year, Instant(year=2021, month=9, day=1), 3)): (2021, 9, 13)} All the rest of the ``tuple`` protocols are inherited as well: @@ -88,7 +87,7 @@ def __str__(self) -> str: Examples: >>> from openfisca_core.periods import Instant - >>> jan = Instant((2021, 1, 1)) + >>> jan = Instant(2021, 1, 1) >>> feb = jan.offset(1, MONTH) >>> str(Period((YEAR, jan, 1))) @@ -162,7 +161,7 @@ def __contains__(self, other: object) -> bool: Example: >>> from openfisca_core.periods import Instant - >>> start = Instant((2021, 1, 1)) + >>> start = Instant(2021, 1, 1) >>> period = Period((YEAR, start, 1)) >>> sub_period = Period((MONTH, start, 3)) @@ -186,7 +185,7 @@ def unit(self) -> DateUnit: Example: >>> from openfisca_core.periods import Instant - >>> start = Instant((2021, 10, 1)) + >>> start = Instant(2021, 10, 1) >>> period = Period((YEAR, start, 3)) >>> period.unit @@ -206,11 +205,11 @@ def start(self) -> Offsetable[int, int, int]: Example: >>> from openfisca_core.periods import Instant - >>> start = Instant((2021, 10, 1)) + >>> start = Instant(2021, 10, 1) >>> period = Period((YEAR, start, 3)) >>> period.start - Instant((2021, 10, 1)) + Instant(year=2021, month=10, day=1) """ @@ -226,7 +225,7 @@ def size(self) -> int: Example: >>> from openfisca_core.periods import Instant - >>> start = Instant((2021, 10, 1)) + >>> start = Instant(2021, 10, 1) >>> period = Period((YEAR, start, 3)) >>> period.size @@ -246,16 +245,16 @@ def stop(self) -> Offsetable[int, int, int]: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2012, 2, 29)) + >>> start = Instant(2012, 2, 29) >>> Period((YEAR, start, 2)).stop - Instant((2014, 2, 27)) + Instant(year=2014, month=2, day=27) >>> Period((MONTH, start, 36)).stop - Instant((2015, 2, 27)) + Instant(year=2015, month=2, day=27) >>> Period((DAY, start, 1096)).stop - Instant((2015, 2, 28)) + Instant(year=2015, month=2, day=28) """ @@ -278,7 +277,7 @@ def date(self) -> datetime.date: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2021, 10, 1)) + >>> start = Instant(2021, 10, 1) >>> period = Period((YEAR, start, 1)) >>> period.date() @@ -314,7 +313,7 @@ def count(self, unit: DateUnit) -> int: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2021, 10, 1)) + >>> start = Instant(2021, 10, 1) >>> period = Period((YEAR, start, 3)) >>> period.count(DAY) @@ -373,18 +372,18 @@ def first(self, unit: DateUnit) -> Period: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2023, 1, 1)) + >>> start = Instant(2023, 1, 1) >>> period = Period((YEAR, start, 3)) >>> period.first(DAY) - Period((day, Instant((2023, 1, 1)), 1)) + Period((day, Instant(year=2023, month=1, day=1), 1)) >>> period.first(MONTH) - Period((month, Instant((2023, 1, 1)), 1)) + Period((month, Instant(year=2023, month=1, day=1), 1)) >>> period.first(YEAR) - Period((year, Instant((2023, 1, 1)), 1)) + Period((year, Instant(year=2023, month=1, day=1), 1)) .. versionadded:: 39.0.0 @@ -405,27 +404,27 @@ def come(self, unit: DateUnit, size: int = 1) -> Period: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2023, 1, 1)) + >>> start = Instant(2023, 1, 1) >>> period = Period((YEAR, start, 3)) >>> period.come(DAY) - Period((day, Instant((2023, 1, 2)), 1)) + Period((day, Instant(year=2023, month=1, day=2), 1)) >>> period.come(DAY, 7) - Period((day, Instant((2023, 1, 8)), 1)) + Period((day, Instant(year=2023, month=1, day=8), 1)) >>> period.come(MONTH) - Period((month, Instant((2023, 2, 1)), 1)) + Period((month, Instant(year=2023, month=2, day=1), 1)) >>> period.come(MONTH, 3) - Period((month, Instant((2023, 4, 1)), 1)) + Period((month, Instant(year=2023, month=4, day=1), 1)) >>> period.come(YEAR) - Period((year, Instant((2024, 1, 1)), 1)) + Period((year, Instant(year=2024, month=1, day=1), 1)) >>> period.come(YEAR, 1) - Period((year, Instant((2024, 1, 1)), 1)) + Period((year, Instant(year=2024, month=1, day=1), 1)) .. versionadded:: 39.0.0 @@ -446,33 +445,33 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2023, 1, 1)) + >>> start = Instant(2020, 3, 31) >>> period = Period((YEAR, start, 3)) >>> period.ago(DAY) - Period((day, Instant((2022, 12, 31)), 1)) + Period((day, Instant(year=2020, month=3, day=30), 1)) >>> period.ago(DAY, 7) - Period((day, Instant((2022, 12, 25)), 1)) + Period((day, Instant(year=2020, month=3, day=24), 1)) >>> period.ago(MONTH) - Period((month, Instant((2022, 12, 1)), 1)) + Period((month, Instant(year=2020, month=2, day=29), 1)) >>> period.ago(MONTH, 3) - Period((month, Instant((2022, 10, 1)), 1)) + Period((month, Instant(year=2019, month=12, day=31), 1)) >>> period.ago(YEAR) - Period((year, Instant((2022, 1, 1)), 1)) + Period((year, Instant(year=2019, month=3, day=31), 1)) >>> period.ago(YEAR, 1) - Period((year, Instant((2022, 1, 1)), 1)) + Period((year, Instant(year=2019, month=3, day=31), 1)) .. versionadded:: 39.0.0 """ - return self.come(unit, -size) + return type(self)((unit, self.start, 1)).offset(-size) def until(self, unit: DateUnit, size: int = 1) -> Period: """Next ``unit`` ``size``s from ``Period.start``. @@ -487,27 +486,27 @@ def until(self, unit: DateUnit, size: int = 1) -> Period: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2023, 1, 1)) + >>> start = Instant(2023, 1, 1) >>> period = Period((YEAR, start, 3)) >>> period.until(DAY) - Period((day, Instant((2023, 1, 1)), 1)) + Period((day, Instant(year=2023, month=1, day=1), 1)) >>> period.until(DAY, 7) - Period((day, Instant((2023, 1, 1)), 7)) + Period((day, Instant(year=2023, month=1, day=1), 7)) >>> period.until(MONTH) - Period((month, Instant((2023, 1, 1)), 1)) + Period((month, Instant(year=2023, month=1, day=1), 1)) >>> period.until(MONTH, 3) - Period((month, Instant((2023, 1, 1)), 3)) + Period((month, Instant(year=2023, month=1, day=1), 3)) >>> period.until(YEAR) - Period((year, Instant((2023, 1, 1)), 1)) + Period((year, Instant(year=2023, month=1, day=1), 1)) >>> period.until(YEAR, 1) - Period((year, Instant((2023, 1, 1)), 1)) + Period((year, Instant(year=2023, month=1, day=1), 1)) .. versionadded:: 39.0.0 @@ -528,27 +527,27 @@ def last(self, unit: DateUnit, size: int = 1) -> Period: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2023, 1, 1)) + >>> start = Instant(2023, 1, 1) >>> period = Period((YEAR, start, 3)) >>> period.last(DAY) - Period((day, Instant((2022, 12, 31)), 1)) + Period((day, Instant(year=2022, month=12, day=31), 1)) >>> period.last(DAY, 7) - Period((day, Instant((2022, 12, 25)), 7)) + Period((day, Instant(year=2022, month=12, day=25), 7)) >>> period.last(MONTH) - Period((month, Instant((2022, 12, 1)), 1)) + Period((month, Instant(year=2022, month=12, day=1), 1)) >>> period.last(MONTH, 3) - Period((month, Instant((2022, 10, 1)), 3)) + Period((month, Instant(year=2022, month=10, day=1), 3)) >>> period.last(YEAR) - Period((year, Instant((2022, 1, 1)), 1)) + Period((year, Instant(year=2022, month=1, day=1), 1)) >>> period.last(YEAR, 1) - Period((year, Instant((2022, 1, 1)), 1)) + Period((year, Instant(year=2022, month=1, day=1), 1)) .. versionadded:: 39.0.0 @@ -569,21 +568,21 @@ def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2014, 2, 3)) + >>> start = Instant(2014, 2, 3) >>> Period((DAY, start, 1)).offset("first-of", MONTH) - Period((day, Instant((2014, 2, 1)), 1)) + Period((day, Instant(year=2014, month=2, day=1), 1)) >>> Period((MONTH, start, 4)).offset("last-of", MONTH) - Period((month, Instant((2014, 2, 28)), 4)) + Period((month, Instant(year=2014, month=2, day=28), 4)) - >>> start = Instant((2021, 1, 1)) + >>> start = Instant(2021, 1, 1) >>> Period((DAY, start, 365)).offset(-3) - Period((day, Instant((2020, 12, 29)), 365)) + Period((day, Instant(year=2020, month=12, day=29), 365)) >>> Period((DAY, start, 365)).offset(1, YEAR) - Period((day, Instant((2022, 1, 1)), 365)) + Period((day, Instant(year=2022, month=1, day=1), 365)) """ @@ -610,15 +609,15 @@ def subperiods(self, unit: DateUnit) -> Sequence[Period]: Examples: >>> from openfisca_core.periods import Instant - >>> start = Instant((2021, 1, 1)) + >>> start = Instant(2021, 1, 1) >>> period = Period((YEAR, start, 1)) >>> period.subperiods(MONTH) - [Period((month, Instant((2021, 1, 1)), 1)),...2021, 12, 1)), 1))] + [Period((month, Instant(year=2021, month=1, day=1), 1)),...1), 1))] >>> period = Period((YEAR, start, 2)) >>> period.subperiods(YEAR) - [Period((year, Instant((2021, 1, 1)), 1)),...((2022, 1, 1)), 1))] + [Period((year, Instant(year=2021, month=1, day=1), 1)), ...1), 1))] .. versionchanged:: 39.0.0: Renamed from ``get_subperiods`` to ``subperiods``. diff --git a/openfisca_core/periods/tests/test_builders.py b/openfisca_core/periods/tests/test_builders.py index aa87280bca..f275b488e5 100644 --- a/openfisca_core/periods/tests/test_builders.py +++ b/openfisca_core/periods/tests/test_builders.py @@ -9,15 +9,15 @@ @pytest.mark.parametrize("arg, expected", [ - ["1000", Instant((1000, 1, 1))], - ["1000-01", Instant((1000, 1, 1))], - ["1000-01-01", Instant((1000, 1, 1))], - [1000, Instant((1000, 1, 1))], - [(1000,), Instant((1000, 1, 1))], - [(1000, 1), Instant((1000, 1, 1))], - [(1000, 1, 1), Instant((1000, 1, 1))], - [datetime.date(1, 1, 1), Instant((1, 1, 1))], - [Instant((1, 1, 1)), Instant((1, 1, 1))], + ["1000", Instant(1000, 1, 1)], + ["1000-01", Instant(1000, 1, 1)], + ["1000-01-01", Instant(1000, 1, 1)], + [1000, Instant(1000, 1, 1)], + [(1000,), Instant(1000, 1, 1)], + [(1000, 1), Instant(1000, 1, 1)], + [(1000, 1, 1), Instant(1000, 1, 1)], + [datetime.date(1, 1, 1), Instant(1, 1, 1)], + [Instant(1, 1, 1), Instant(1, 1, 1)], ]) def test_build_instant(arg, expected): """Returns the expected ``Instant``.""" @@ -43,7 +43,7 @@ def test_build_instant(arg, expected): [None, TypeError], [eternity, ValueError], [year, ValueError], - [Period((day, Instant((1, 1, 1)), 365)), ValueError], + [Period((day, Instant(1, 1, 1), 365)), ValueError], ]) def test_build_instant_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" @@ -53,33 +53,33 @@ def test_build_instant_with_an_invalid_argument(arg, error): @pytest.mark.parametrize("arg, expected", [ - ["1000", Period((year, Instant((1000, 1, 1)), 1))], - ["1000-01", Period((month, Instant((1000, 1, 1)), 1))], - ["1000-01-01", Period((day, Instant((1000, 1, 1)), 1))], - ["1004-02-29", Period((day, Instant((1004, 2, 29)), 1))], - ["ETERNITY", Period((eternity, Instant((1, 1, 1)), 1))], - ["day:1000-01-01", Period((day, Instant((1000, 1, 1)), 1))], - ["day:1000-01-01:3", Period((day, Instant((1000, 1, 1)), 3))], - ["eternity", Period((eternity, Instant((1, 1, 1)), 1))], - ["month:1000-01", Period((month, Instant((1000, 1, 1)), 1))], - ["month:1000-01-01", Period((month, Instant((1000, 1, 1)), 1))], - ["month:1000-01-01:3", Period((month, Instant((1000, 1, 1)), 3))], - ["month:1000-01:3", Period((month, Instant((1000, 1, 1)), 3))], - ["year:1000", Period((year, Instant((1000, 1, 1)), 1))], - ["year:1000-01", Period((year, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01", Period((year, Instant((1000, 1, 1)), 1))], - ["year:1000-01-01:3", Period((year, Instant((1000, 1, 1)), 3))], - ["year:1000-01:3", Period((year, Instant((1000, 1, 1)), 3))], - ["year:1000:3", Period((year, Instant((1000, 1, 1)), 3))], - [1000, Period((year, Instant((1000, 1, 1)), 1))], - [eternity, Period((eternity, Instant((1, 1, 1)), 1))], - [Instant((1, 1, 1)), Period((day, Instant((1, 1, 1)), 1))], - [Period((day, Instant((1, 1, 1)), 365)), Period((day, Instant((1, 1, 1)), 365))], - ["month:1000:1", Period((2, Instant((1000, 1, 1)), 1))], - ["month:1000", Period((2, Instant((1000, 1, 1)), 1))], - ["day:1000:1", Period((1, Instant((1000, 1, 1)), 1))], - ["day:1000-01:1", Period((1, Instant((1000, 1, 1)), 1))], - ["day:1000-01", Period((1, Instant((1000, 1, 1)), 1))], + ["1000", Period((year, Instant(1000, 1, 1), 1))], + ["1000-01", Period((month, Instant(1000, 1, 1), 1))], + ["1000-01-01", Period((day, Instant(1000, 1, 1), 1))], + ["1004-02-29", Period((day, Instant(1004, 2, 29), 1))], + ["ETERNITY", Period((eternity, Instant(1, 1, 1), 1))], + ["day:1000-01-01", Period((day, Instant(1000, 1, 1), 1))], + ["day:1000-01-01:3", Period((day, Instant(1000, 1, 1), 3))], + ["eternity", Period((eternity, Instant(1, 1, 1), 1))], + ["month:1000-01", Period((month, Instant(1000, 1, 1), 1))], + ["month:1000-01-01", Period((month, Instant(1000, 1, 1), 1))], + ["month:1000-01-01:3", Period((month, Instant(1000, 1, 1), 3))], + ["month:1000-01:3", Period((month, Instant(1000, 1, 1), 3))], + ["year:1000", Period((year, Instant(1000, 1, 1), 1))], + ["year:1000-01", Period((year, Instant(1000, 1, 1), 1))], + ["year:1000-01-01", Period((year, Instant(1000, 1, 1), 1))], + ["year:1000-01-01:3", Period((year, Instant(1000, 1, 1), 3))], + ["year:1000-01:3", Period((year, Instant(1000, 1, 1), 3))], + ["year:1000:3", Period((year, Instant(1000, 1, 1), 3))], + [1000, Period((year, Instant(1000, 1, 1), 1))], + [eternity, Period((eternity, Instant(1, 1, 1), 1))], + [Instant(1, 1, 1), Period((day, Instant(1, 1, 1), 1))], + [Period((day, Instant(1, 1, 1), 365)), Period((day, Instant(1, 1, 1), 365))], + ["month:1000:1", Period((2, Instant(1000, 1, 1), 1))], + ["month:1000", Period((2, Instant(1000, 1, 1), 1))], + ["day:1000:1", Period((1, Instant(1000, 1, 1), 1))], + ["day:1000-01:1", Period((1, Instant(1000, 1, 1), 1))], + ["day:1000-01", Period((1, Instant(1000, 1, 1), 1))], ]) def test_build_period(arg, expected): """Returns the expected ``Period``.""" diff --git a/openfisca_core/periods/tests/test_instant.py b/openfisca_core/periods/tests/test_instant.py index 73c61f4250..181a77194d 100644 --- a/openfisca_core/periods/tests/test_instant.py +++ b/openfisca_core/periods/tests/test_instant.py @@ -9,20 +9,20 @@ def instant(): """Returns a ``Instant``.""" - return Instant((2020, 2, 29)) + return Instant(2020, 2, 29) @pytest.mark.parametrize("offset, unit, expected", [ - ["first-of", month, Instant((2020, 2, 1))], - ["first-of", year, Instant((2020, 1, 1))], - ["last-of", month, Instant((2020, 2, 29))], - ["last-of", year, Instant((2020, 12, 31))], - [-3, day, Instant((2020, 2, 26))], - [-3, month, Instant((2019, 11, 29))], - [-3, year, Instant((2017, 2, 28))], - [3, day, Instant((2020, 3, 3))], - [3, month, Instant((2020, 5, 29))], - [3, year, Instant((2023, 2, 28))], + ["first-of", month, Instant(2020, 2, 1)], + ["first-of", year, Instant(2020, 1, 1)], + ["last-of", month, Instant(2020, 2, 29)], + ["last-of", year, Instant(2020, 12, 31)], + [-3, day, Instant(2020, 2, 26)], + [-3, month, Instant(2019, 11, 29)], + [-3, year, Instant(2017, 2, 28)], + [3, day, Instant(2020, 3, 3)], + [3, month, Instant(2020, 5, 29)], + [3, year, Instant(2023, 2, 28)], ]) def test_offset(instant, offset, unit, expected): """Returns the expected ``Instant``.""" diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 6d30a8a981..be7d85e86d 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -9,16 +9,16 @@ def instant(): """Returns a ``Instant``.""" - return Instant((2022, 12, 31)) + return Instant(2022, 12, 31) @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [month, Instant((2022, 1, 1)), 12, "2022"], - [month, Instant((2022, 3, 1)), 12, "year:2022-03"], - [year, Instant((2022, 1, 1)), 1, "2022"], - [year, Instant((2022, 1, 1)), 3, "year:2022:3"], - [year, Instant((2022, 1, 3)), 3, "year:2022:3"], - [year, Instant((2022, 3, 1)), 1, "year:2022-03"], + [month, Instant(2022, 1, 1), 12, "2022"], + [month, Instant(2022, 3, 1), 12, "year:2022-03"], + [year, Instant(2022, 1, 1), 1, "2022"], + [year, Instant(2022, 1, 1), 3, "year:2022:3"], + [year, Instant(2022, 1, 3), 3, "year:2022:3"], + [year, Instant(2022, 3, 1), 1, "year:2022-03"], ]) def test_str_with_years(date_unit, instant, size, expected): """Returns the expected string.""" @@ -27,9 +27,9 @@ def test_str_with_years(date_unit, instant, size, expected): @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [month, Instant((2022, 1, 1)), 1, "2022-01"], - [month, Instant((2022, 1, 1)), 3, "month:2022-01:3"], - [month, Instant((2022, 3, 1)), 3, "month:2022-03:3"], + [month, Instant(2022, 1, 1), 1, "2022-01"], + [month, Instant(2022, 1, 1), 3, "month:2022-01:3"], + [month, Instant(2022, 3, 1), 3, "month:2022-03:3"], ]) def test_str_with_months(date_unit, instant, size, expected): """Returns the expected string.""" @@ -38,9 +38,9 @@ def test_str_with_months(date_unit, instant, size, expected): @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [day, Instant((2022, 1, 1)), 1, "2022-01-01"], - [day, Instant((2022, 1, 1)), 3, "day:2022-01-01:3"], - [day, Instant((2022, 3, 1)), 3, "day:2022-03-01:3"], + [day, Instant(2022, 1, 1), 1, "2022-01-01"], + [day, Instant(2022, 1, 1), 3, "day:2022-01-01:3"], + [day, Instant(2022, 3, 1), 3, "day:2022-03-01:3"], ]) def test_str_with_days(date_unit, instant, size, expected): """Returns the expected string.""" @@ -49,12 +49,12 @@ def test_str_with_days(date_unit, instant, size, expected): @pytest.mark.parametrize("period_unit, unit, start, cease, count", [ - [day, day, Instant((2022, 12, 31)), Instant((2023, 1, 2)), 3], - [month, day, Instant((2022, 12, 31)), Instant((2023, 3, 30)), 90], - [month, month, Instant((2022, 12, 1)), Instant((2023, 2, 1)), 3], - [year, day, Instant((2022, 12, 31)), Instant((2025, 12, 30)), 1096], - [year, month, Instant((2022, 12, 1)), Instant((2025, 11, 1)), 36], - [year, year, Instant((2022, 1, 1)), Instant((2024, 1, 1)), 3], + [day, day, Instant(2022, 12, 31), Instant(2023, 1, 2), 3], + [month, day, Instant(2022, 12, 31), Instant(2023, 3, 30), 90], + [month, month, Instant(2022, 12, 1), Instant(2023, 2, 1), 3], + [year, day, Instant(2022, 12, 31), Instant(2025, 12, 30), 1096], + [year, month, Instant(2022, 12, 1), Instant(2025, 11, 1), 36], + [year, year, Instant(2022, 1, 1), Instant(2024, 1, 1), 3], ]) def test_subperiods(instant, period_unit, unit, start, cease, count): """Returns the expected subperiods.""" @@ -68,27 +68,27 @@ def test_subperiods(instant, period_unit, unit, start, cease, count): @pytest.mark.parametrize("period_unit, offset, unit, expected", [ - [day, "first-of", month, Period((day, Instant((2022, 12, 1)), 3))], - [day, "first-of", year, Period((day, Instant((2022, 1, 1)), 3))], - [day, "last-of", month, Period((day, Instant((2022, 12, 31)), 3))], - [day, "last-of", year, Period((day, Instant((2022, 12, 31)), 3))], - [day, -3, year, Period((day, Instant((2019, 12, 31)), 3))], - [day, 1, month, Period((day, Instant((2023, 1, 31)), 3))], - [day, 3, day, Period((day, Instant((2023, 1, 3)), 3))], - [month, "first-of", month, Period((month, Instant((2022, 12, 1)), 3))], - [month, "first-of", year, Period((month, Instant((2022, 1, 1)), 3))], - [month, "last-of", month, Period((month, Instant((2022, 12, 31)), 3))], - [month, "last-of", year, Period((month, Instant((2022, 12, 31)), 3))], - [month, -3, year, Period((month, Instant((2019, 12, 31)), 3))], - [month, 1, month, Period((month, Instant((2023, 1, 31)), 3))], - [month, 3, day, Period((month, Instant((2023, 1, 3)), 3))], - [year, "first-of", month, Period((year, Instant((2022, 12, 1)), 3))], - [year, "first-of", year, Period((year, Instant((2022, 1, 1)), 3))], - [year, "last-of", month, Period((year, Instant((2022, 12, 31)), 3))], - [year, "last-of", year, Period((year, Instant((2022, 12, 31)), 3))], - [year, -3, year, Period((year, Instant((2019, 12, 31)), 3))], - [year, 1, month, Period((year, Instant((2023, 1, 31)), 3))], - [year, 3, day, Period((year, Instant((2023, 1, 3)), 3))], + [day, "first-of", month, Period((day, Instant(2022, 12, 1), 3))], + [day, "first-of", year, Period((day, Instant(2022, 1, 1), 3))], + [day, "last-of", month, Period((day, Instant(2022, 12, 31), 3))], + [day, "last-of", year, Period((day, Instant(2022, 12, 31), 3))], + [day, -3, year, Period((day, Instant(2019, 12, 31), 3))], + [day, 1, month, Period((day, Instant(2023, 1, 31), 3))], + [day, 3, day, Period((day, Instant(2023, 1, 3), 3))], + [month, "first-of", month, Period((month, Instant(2022, 12, 1), 3))], + [month, "first-of", year, Period((month, Instant(2022, 1, 1), 3))], + [month, "last-of", month, Period((month, Instant(2022, 12, 31), 3))], + [month, "last-of", year, Period((month, Instant(2022, 12, 31), 3))], + [month, -3, year, Period((month, Instant(2019, 12, 31), 3))], + [month, 1, month, Period((month, Instant(2023, 1, 31), 3))], + [month, 3, day, Period((month, Instant(2023, 1, 3), 3))], + [year, "first-of", month, Period((year, Instant(2022, 12, 1), 3))], + [year, "first-of", year, Period((year, Instant(2022, 1, 1), 3))], + [year, "last-of", month, Period((year, Instant(2022, 12, 31), 3))], + [year, "last-of", year, Period((year, Instant(2022, 12, 31), 3))], + [year, -3, year, Period((year, Instant(2019, 12, 31), 3))], + [year, 1, month, Period((year, Instant(2023, 1, 31), 3))], + [year, 3, day, Period((year, Instant(2023, 1, 3), 3))], ]) def test_offset(instant, period_unit, offset, unit, expected): """Returns the expected ``Period``.""" @@ -99,13 +99,13 @@ def test_offset(instant, period_unit, offset, unit, expected): @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [month, Instant((2012, 1, 3)), 3, 3], - [month, Instant((2012, 2, 3)), 1, 1], - [month, Instant((2022, 1, 3)), 3, 3], - [month, Instant((2022, 12, 1)), 1, 1], - [year, Instant((2012, 1, 1)), 1, 12], - [year, Instant((2022, 1, 1)), 2, 24], - [year, Instant((2022, 12, 1)), 1, 12], + [month, Instant(2012, 1, 3), 3, 3], + [month, Instant(2012, 2, 3), 1, 1], + [month, Instant(2022, 1, 3), 3, 3], + [month, Instant(2022, 12, 1), 1, 1], + [year, Instant(2012, 1, 1), 1, 12], + [year, Instant(2022, 1, 1), 2, 24], + [year, Instant(2022, 12, 1), 1, 12], ]) def test_day_size_in_months(date_unit, instant, size, expected): """Returns the expected number of months.""" @@ -116,15 +116,15 @@ def test_day_size_in_months(date_unit, instant, size, expected): @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [day, Instant((2022, 12, 31)), 1, 1], - [day, Instant((2022, 12, 31)), 3, 3], - [month, Instant((2012, 1, 3)), 3, 31 + 29 + 31], - [month, Instant((2012, 2, 3)), 1, 29], - [month, Instant((2022, 1, 3)), 3, 31 + 28 + 31], - [month, Instant((2022, 12, 1)), 1, 31], - [year, Instant((2012, 1, 1)), 1, 366], - [year, Instant((2022, 1, 1)), 2, 730], - [year, Instant((2022, 12, 1)), 1, 365], + [day, Instant(2022, 12, 31), 1, 1], + [day, Instant(2022, 12, 31), 3, 3], + [month, Instant(2012, 1, 3), 3, 31 + 29 + 31], + [month, Instant(2012, 2, 3), 1, 29], + [month, Instant(2022, 1, 3), 3, 31 + 28 + 31], + [month, Instant(2022, 12, 1), 1, 31], + [year, Instant(2012, 1, 1), 1, 366], + [year, Instant(2022, 1, 1), 2, 730], + [year, Instant(2022, 12, 1), 1, 365], ]) def test_day_size_in_days(date_unit, instant, size, expected): """Returns the expected number of days.""" diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index d3098ab70a..7fe032ef33 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -17,7 +17,7 @@ class Offsetable(Protocol[T, U, V]): @abc.abstractmethod - def __init__(self, values: Tuple[T, U, V]) -> None: + def __init__(self, *args: Tuple[T, U, V]) -> None: ... @abc.abstractmethod diff --git a/openfisca_core/simulations/simulation.py b/openfisca_core/simulations/simulation.py index 26b46b883d..6404dfc181 100644 --- a/openfisca_core/simulations/simulation.py +++ b/openfisca_core/simulations/simulation.py @@ -417,7 +417,7 @@ def get_known_periods(self, variable): >>> simulation.set_input('age', '2018-04', [12, 14]) >>> simulation.set_input('age', '2018-05', [13, 14]) >>> simulation.get_known_periods('age') - [periods.period((u'month', Instant((2018, 5, 1)), 1)), periods.period((u'month', Instant((2018, 4, 1)), 1))] + [periods.period((u'month', Instant(2018, 5, 1)), 1)), periods.period((u'month', Instant((2018, 4, 1)), 1)] """ return self.get_holder(variable).get_known_periods() diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index d636224234..944f649f16 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -412,7 +412,7 @@ def get_parameters_at_instant( key = periods.instant(instant) else: - msg = f"Expected an Instant (e.g. Instant((2017, 1, 1)) ). Got: {instant}." + msg = f"Expected an Instant (e.g. Instant(2017, 1, 1) ). Got: {instant}." raise AssertionError(msg) if self.parameters is None: From 7e17e58a62a3c4b9f10ee8371e1be973bc262516 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 22 Dec 2022 21:27:02 +0100 Subject: [PATCH 88/93] Fix period implementation --- CHANGELOG.md | 2 +- openfisca_core/holders/helpers.py | 6 +- openfisca_core/holders/tests/test_helpers.py | 4 +- openfisca_core/periods/helpers.py | 32 +-- openfisca_core/periods/period_.py | 250 +++++++----------- openfisca_core/periods/tests/test_builders.py | 56 ++-- openfisca_core/periods/tests/test_period.py | 60 ++--- openfisca_core/periods/typing.py | 6 +- 8 files changed, 176 insertions(+), 240 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 593e803b66..aba4c8f2b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,7 +92,7 @@ #### Migration details -- Replace `some_period.start.period` and similar methods with `Period((unit, some_period.start, 1))`. +- Replace `some_period.start.period` and similar methods with `Period(unit, some_period.start, 1)`. # 36.0.0 [#1149](https://github.com/openfisca/openfisca-core/pull/1162) diff --git a/openfisca_core/holders/helpers.py b/openfisca_core/holders/helpers.py index 813426d805..1d8d1a58f0 100644 --- a/openfisca_core/holders/helpers.py +++ b/openfisca_core/holders/helpers.py @@ -27,7 +27,7 @@ def set_input_dispatch_by_period(holder, period, array): after_instant = period.start.offset(period_size, period_unit) # Cache the input data, skipping the existing cached months - sub_period = periods.Period((cached_period_unit, period.start, 1)) + sub_period = periods.Period(cached_period_unit, period.start, 1) while sub_period.start < after_instant: existing_array = holder.get_array(sub_period) if existing_array is None: @@ -60,7 +60,7 @@ def set_input_divide_by_period(holder, period, array): # Count the number of elementary periods to change, and the difference with what is already known. remaining_array = array.copy() - sub_period = periods.Period((cached_period_unit, period.start, 1)) + sub_period = periods.Period(cached_period_unit, period.start, 1) sub_periods_count = 0 while sub_period.start < after_instant: existing_array = holder.get_array(sub_period) @@ -73,7 +73,7 @@ def set_input_divide_by_period(holder, period, array): # Cache the input data if sub_periods_count > 0: divided_array = remaining_array / sub_periods_count - sub_period = periods.Period((cached_period_unit, period.start, 1)) + sub_period = periods.Period(cached_period_unit, period.start, 1) while sub_period.start < after_instant: if holder.get_array(sub_period) is None: holder._set(sub_period, divided_array) diff --git a/openfisca_core/holders/tests/test_helpers.py b/openfisca_core/holders/tests/test_helpers.py index 0847baf90f..84e94bba02 100644 --- a/openfisca_core/holders/tests/test_helpers.py +++ b/openfisca_core/holders/tests/test_helpers.py @@ -58,7 +58,7 @@ def test_set_input_dispatch_by_period( income = Income() holder = Holder(income, population) instant = periods.Instant(2022, 1, 1) - dispatch_period = periods.Period((dispatch_unit, instant, 3)) + dispatch_period = periods.Period(dispatch_unit, instant, 3) holders.set_input_dispatch_by_period(holder, dispatch_period, values) total = sum(map(holder.get_array, holder.get_known_periods())) @@ -89,7 +89,7 @@ def test_set_input_divide_by_period( income = Income() holder = Holder(income, population) instant = periods.Instant(2022, 1, 1) - divide_period = periods.Period((divide_unit, instant, 3)) + divide_period = periods.Period(divide_unit, instant, 3) holders.set_input_divide_by_period(holder, divide_period, values) last = holder.get_array(holder.get_known_periods()[-1]) diff --git a/openfisca_core/periods/helpers.py b/openfisca_core/periods/helpers.py index 37c76a3106..d57648057d 100644 --- a/openfisca_core/periods/helpers.py +++ b/openfisca_core/periods/helpers.py @@ -52,7 +52,7 @@ def build_instant(value: Any) -> Instant: >>> start = Instant(2021, 9, 16) - >>> build_instant(Period((year, start, 1))) + >>> build_instant(Period(year, start, 1)) Traceback (most recent call last): InstantFormatError: 'year:2021-09' is not a valid instant. @@ -100,40 +100,40 @@ def build_period(value: Any) -> Period: PeriodTypeError: When ``value`` is not a ``period-like`` object. Examples: - >>> build_period(Period((year, Instant(2021, 1, 1), 1))) - Period((year, Instant(year=2021, month=1, day=1), 1)) + >>> build_period(Period(year, Instant(2021, 1, 1), 1)) + Period(unit=year, start=Instant(year=2021, month=1, day=1), size=1) >>> build_period(Instant(2021, 1, 1)) - Period((day, Instant(year=2021, month=1, day=1), 1)) + Period(unit=day, start=Instant(year=2021, month=1, day=1), size=1) >>> build_period(eternity) - Period((eternity, Instant(year=1, month=1, day=1), 1)) + Period(unit=eternity, start=Instant(year=1, month=1, day=1), size=1) >>> build_period(2021) - Period((year, Instant(year=2021, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2021, month=1, day=1), size=1) >>> build_period("2014") - Period((year, Instant(year=2014, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2014, month=1, day=1), size=1) >>> build_period("year:2014") - Period((year, Instant(year=2014, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2014, month=1, day=1), size=1) >>> build_period("month:2014-02") - Period((month, Instant(year=2014, month=2, day=1), 1)) + Period(unit=month, start=Instant(year=2014, month=2, day=1), size=1) >>> build_period("year:2014-02") - Period((year, Instant(year=2014, month=2, day=1), 1)) + Period(unit=year, start=Instant(year=2014, month=2, day=1), size=1) >>> build_period("day:2014-02-02") - Period((day, Instant(year=2014, month=2, day=2), 1)) + Period(unit=day, start=Instant(year=2014, month=2, day=2), size=1) >>> build_period("day:2014-02-02:3") - Period((day, Instant(year=2014, month=2, day=2), 3)) + Period(unit=day, start=Instant(year=2014, month=2, day=2), size=3) """ if value in {eternity, eternity.name, eternity.name.lower()}: - return Period((eternity, build_instant(datetime.date.min), 1)) + return Period(eternity, build_instant(datetime.date.min), 1) if value is None or isinstance(value, DateUnit): raise PeriodTypeError(value) @@ -142,10 +142,10 @@ def build_period(value: Any) -> Period: return value if isinstance(value, Instant): - return Period((day, value, 1)) + return Period(day, value, 1) if isinstance(value, int): - return Period((year, Instant(value, 1, 1), 1)) + return Period(year, Instant(value, 1, 1), 1) if not isinstance(value, str): raise PeriodFormatError(value) @@ -158,7 +158,7 @@ def build_period(value: Any) -> Period: unit = DateUnit(isoformat.unit) - return Period((unit, build_instant(isoformat[:3]), isoformat.size)) + return Period(unit, build_instant(isoformat[:3]), isoformat.size) def parse_int(value: int) -> ISOFormat | None: diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index bbc523d5b1..1b2fa159a2 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Sequence, Tuple +from typing import NamedTuple, Sequence import datetime @@ -10,32 +10,23 @@ DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) +Instant = Offsetable[int, int, int] -class Period(Tuple[DateUnit, Offsetable[int, int, int], int]): - """Toolbox to handle date intervals. - - A ``Period`` is a triple (``unit``, ``start``, ``size``). - Attributes: - unit: Either ``year``, ``month``, ``day`` or ``eternity``. - start: The "instant" the Period starts at. - size: The amount of ``unit``, starting at ``start``, at least ``1``. - - Args: - (tuple(DateUnit, .Instant, int)): - The ``unit``, ``start``, and ``size``, accordingly. +class Period(NamedTuple): + """Toolbox to handle date intervals. Examples: >>> from openfisca_core.periods import Instant >>> start = Instant(2021, 9, 1) - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) ``Periods`` are represented as a ``tuple`` containing the ``unit``, an ``Instant`` and the ``size``: >>> repr(period) - 'Period((year, Instant(year=2021, month=9, day=1), 3))' + 'Period(unit=year, start=Instant(year=2021, month=9, day=1), size=3)' Their user-friendly representation is as a date in the ISO format, prefixed with the ``unit`` and suffixed with its ``size``: @@ -47,7 +38,7 @@ class Period(Tuple[DateUnit, Offsetable[int, int, int], int]): contain a nested data structure, they're not hashable: >>> {period: (2021, 9, 13)} - {Period((year, Instant(year=2021, month=9, day=1), 3)): (2021, 9, 13)} + {Period(unit=year, start=Instant(year=2021, month=9, day=1), size=3):...} All the rest of the ``tuple`` protocols are inherited as well: @@ -60,10 +51,10 @@ class Period(Tuple[DateUnit, Offsetable[int, int, int], int]): >>> len(period) 3 - >>> period == Period((YEAR, start, 3)) + >>> period == Period(YEAR, start, 3) True - >>> period > Period((YEAR, start, 3)) + >>> period > Period(YEAR, start, 3) False >>> unit, (year, month, day), size = period @@ -72,11 +63,14 @@ class Period(Tuple[DateUnit, Offsetable[int, int, int], int]): """ - def __repr__(self) -> str: - return ( - f"{type(self).__name__}" - f"({super(type(self), self).__repr__()})" - ) + #: Either ``year``, ``month``, ``day`` or ``eternity``. + unit: DateUnit + + #: The "instant" the Period starts at. + start: Offsetable[int, int, int] + + #: The amount of ``unit``, starting at ``start``, at least ``1``. + size: int def __str__(self) -> str: """Transform period to a string. @@ -90,33 +84,31 @@ def __str__(self) -> str: >>> jan = Instant(2021, 1, 1) >>> feb = jan.offset(1, MONTH) - >>> str(Period((YEAR, jan, 1))) + >>> str(Period(YEAR, jan, 1)) '2021' - >>> str(Period((YEAR, feb, 1))) + >>> str(Period(YEAR, feb, 1)) 'year:2021-02' - >>> str(Period((MONTH, feb, 1))) + >>> str(Period(MONTH, feb, 1)) '2021-02' - >>> str(Period((YEAR, jan, 2))) + >>> str(Period(YEAR, jan, 2)) 'year:2021:2' - >>> str(Period((MONTH, jan, 2))) + >>> str(Period(MONTH, jan, 2)) 'month:2021-01:2' - >>> str(Period((MONTH, jan, 12))) + >>> str(Period(MONTH, jan, 12)) '2021' """ - size = self.size - start = self.start - unit = self.unit + year: int + month: int + day: int - day = start.day - month = start.month - year = start.year + unit, (year, month, day), size = self if unit == ETERNITY: return str(unit.name) @@ -162,8 +154,8 @@ def __contains__(self, other: object) -> bool: >>> from openfisca_core.periods import Instant >>> start = Instant(2021, 1, 1) - >>> period = Period((YEAR, start, 1)) - >>> sub_period = Period((MONTH, start, 3)) + >>> period = Period(YEAR, start, 1) + >>> sub_period = Period(MONTH, start, 3) >>> sub_period in period True @@ -176,67 +168,7 @@ def __contains__(self, other: object) -> bool: return super().__contains__(other) @property - def unit(self) -> DateUnit: - """The ``unit`` of the ``Period``. - - Returns: - A DateUnit. - - Example: - >>> from openfisca_core.periods import Instant - - >>> start = Instant(2021, 10, 1) - >>> period = Period((YEAR, start, 3)) - - >>> period.unit - year - - """ - - return self[0] - - @property - def start(self) -> Offsetable[int, int, int]: - """The ``Instant`` at which the ``Period`` starts. - - Returns: - An Instant. - - Example: - >>> from openfisca_core.periods import Instant - - >>> start = Instant(2021, 10, 1) - >>> period = Period((YEAR, start, 3)) - - >>> period.start - Instant(year=2021, month=10, day=1) - - """ - - return self[1] - - @property - def size(self) -> int: - """The ``size`` of the ``Period``. - - Returns: - An int. - - Example: - >>> from openfisca_core.periods import Instant - - >>> start = Instant(2021, 10, 1) - >>> period = Period((YEAR, start, 3)) - - >>> period.size - 3 - - """ - - return self[2] - - @property - def stop(self) -> Offsetable[int, int, int]: + def stop(self) -> Instant: """Last day of the ``Period`` as an ``Instant``. Returns: @@ -247,13 +179,13 @@ def stop(self) -> Offsetable[int, int, int]: >>> start = Instant(2012, 2, 29) - >>> Period((YEAR, start, 2)).stop + >>> Period(YEAR, start, 2).stop Instant(year=2014, month=2, day=27) - >>> Period((MONTH, start, 36)).stop + >>> Period(MONTH, start, 36).stop Instant(year=2015, month=2, day=27) - >>> Period((DAY, start, 1096)).stop + >>> Period(DAY, start, 1096).stop Instant(year=2015, month=2, day=28) """ @@ -279,11 +211,11 @@ def date(self) -> datetime.date: >>> start = Instant(2021, 10, 1) - >>> period = Period((YEAR, start, 1)) + >>> period = Period(YEAR, start, 1) >>> period.date() Date(2021, 10, 1) - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) >>> period.date() Traceback (most recent call last): ValueError: 'date' undefined for period size > 1: year:2021-10:3. @@ -315,28 +247,28 @@ def count(self, unit: DateUnit) -> int: >>> start = Instant(2021, 10, 1) - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) >>> period.count(DAY) 1096 - >>> period = Period((MONTH, start, 3)) + >>> period = Period(MONTH, start, 3) >>> period.count(DAY) 92 - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) >>> period.count(MONTH) 36 - >>> period = Period((DAY, start, 3)) + >>> period = Period(DAY, start, 3) >>> period.count(MONTH) Traceback (most recent call last): ValueError: Cannot calculate number of months in a day. - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) >>> period.count(YEAR) 3 - >>> period = Period((MONTH, start, 3)) + >>> period = Period(MONTH, start, 3) >>> period.count(YEAR) Traceback (most recent call last): ValueError: Cannot calculate number of years in a month. @@ -374,22 +306,22 @@ def first(self, unit: DateUnit) -> Period: >>> start = Instant(2023, 1, 1) - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) >>> period.first(DAY) - Period((day, Instant(year=2023, month=1, day=1), 1)) + Period(unit=day, start=Instant(year=2023, month=1, day=1), size=1) >>> period.first(MONTH) - Period((month, Instant(year=2023, month=1, day=1), 1)) + Period(unit=month, start=Instant(year=2023, month=1, day=1), size=1) >>> period.first(YEAR) - Period((year, Instant(year=2023, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2023, month=1, day=1), size=1) .. versionadded:: 39.0.0 """ - return type(self)((unit, self.start.offset("first-of", unit), 1)) + return type(self)(unit, self.start.offset("first-of", unit), 1) def come(self, unit: DateUnit, size: int = 1) -> Period: """The next ``unit``s ``size`` from ``Period.start``. @@ -406,31 +338,31 @@ def come(self, unit: DateUnit, size: int = 1) -> Period: >>> start = Instant(2023, 1, 1) - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) >>> period.come(DAY) - Period((day, Instant(year=2023, month=1, day=2), 1)) + Period(unit=day, start=Instant(year=2023, month=1, day=2), size=1) >>> period.come(DAY, 7) - Period((day, Instant(year=2023, month=1, day=8), 1)) + Period(unit=day, start=Instant(year=2023, month=1, day=8), size=1) >>> period.come(MONTH) - Period((month, Instant(year=2023, month=2, day=1), 1)) + Period(unit=month, start=Instant(year=2023, month=2, day=1), size=1) >>> period.come(MONTH, 3) - Period((month, Instant(year=2023, month=4, day=1), 1)) + Period(unit=month, start=Instant(year=2023, month=4, day=1), size=1) >>> period.come(YEAR) - Period((year, Instant(year=2024, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2024, month=1, day=1), size=1) >>> period.come(YEAR, 1) - Period((year, Instant(year=2024, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2024, month=1, day=1), size=1) .. versionadded:: 39.0.0 """ - return type(self)((unit, self.first(unit).start, 1)).offset(size) + return type(self)(unit, self.first(unit).start, 1).offset(size) def ago(self, unit: DateUnit, size: int = 1) -> Period: """``size`` ``unit``s ago from ``Period.start``. @@ -447,31 +379,31 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: >>> start = Instant(2020, 3, 31) - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) >>> period.ago(DAY) - Period((day, Instant(year=2020, month=3, day=30), 1)) + Period(unit=day, start=Instant(year=2020, month=3, day=30), size=1) >>> period.ago(DAY, 7) - Period((day, Instant(year=2020, month=3, day=24), 1)) + Period(unit=day, start=Instant(year=2020, month=3, day=24), size=1) >>> period.ago(MONTH) - Period((month, Instant(year=2020, month=2, day=29), 1)) + Period(unit=month, start=Instant(year=2020, month=2, day=29), size=1) >>> period.ago(MONTH, 3) - Period((month, Instant(year=2019, month=12, day=31), 1)) + Period(unit=month, start=Instant(year=2019, month=12, day=31), size=1) >>> period.ago(YEAR) - Period((year, Instant(year=2019, month=3, day=31), 1)) + Period(unit=year, start=Instant(year=2019, month=3, day=31), size=1) >>> period.ago(YEAR, 1) - Period((year, Instant(year=2019, month=3, day=31), 1)) + Period(unit=year, start=Instant(year=2019, month=3, day=31), size=1) .. versionadded:: 39.0.0 """ - return type(self)((unit, self.start, 1)).offset(-size) + return type(self)(unit, self.start, 1).offset(-size) def until(self, unit: DateUnit, size: int = 1) -> Period: """Next ``unit`` ``size``s from ``Period.start``. @@ -488,31 +420,31 @@ def until(self, unit: DateUnit, size: int = 1) -> Period: >>> start = Instant(2023, 1, 1) - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) >>> period.until(DAY) - Period((day, Instant(year=2023, month=1, day=1), 1)) + Period(unit=day, start=Instant(year=2023, month=1, day=1), size=1) >>> period.until(DAY, 7) - Period((day, Instant(year=2023, month=1, day=1), 7)) + Period(unit=day, start=Instant(year=2023, month=1, day=1), size=7) >>> period.until(MONTH) - Period((month, Instant(year=2023, month=1, day=1), 1)) + Period(unit=month, start=Instant(year=2023, month=1, day=1), size=1) >>> period.until(MONTH, 3) - Period((month, Instant(year=2023, month=1, day=1), 3)) + Period(unit=month, start=Instant(year=2023, month=1, day=1), size=3) >>> period.until(YEAR) - Period((year, Instant(year=2023, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2023, month=1, day=1), size=1) >>> period.until(YEAR, 1) - Period((year, Instant(year=2023, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2023, month=1, day=1), size=1) .. versionadded:: 39.0.0 """ - return type(self)((unit, self.first(unit).start, size)) + return type(self)(unit, self.first(unit).start, size) def last(self, unit: DateUnit, size: int = 1) -> Period: """Last ``size`` ``unit``s from ``Period.start``. @@ -529,31 +461,31 @@ def last(self, unit: DateUnit, size: int = 1) -> Period: >>> start = Instant(2023, 1, 1) - >>> period = Period((YEAR, start, 3)) + >>> period = Period(YEAR, start, 3) >>> period.last(DAY) - Period((day, Instant(year=2022, month=12, day=31), 1)) + Period(unit=day, start=Instant(year=2022, month=12, day=31), size=1) >>> period.last(DAY, 7) - Period((day, Instant(year=2022, month=12, day=25), 7)) + Period(unit=day, start=Instant(year=2022, month=12, day=25), size=7) >>> period.last(MONTH) - Period((month, Instant(year=2022, month=12, day=1), 1)) + Period(unit=month, start=Instant(year=2022, month=12, day=1), size=1) >>> period.last(MONTH, 3) - Period((month, Instant(year=2022, month=10, day=1), 3)) + Period(unit=month, start=Instant(year=2022, month=10, day=1), size=3) >>> period.last(YEAR) - Period((year, Instant(year=2022, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2022, month=1, day=1), size=1) >>> period.last(YEAR, 1) - Period((year, Instant(year=2022, month=1, day=1), 1)) + Period(unit=year, start=Instant(year=2022, month=1, day=1), size=1) .. versionadded:: 39.0.0 """ - return type(self)((unit, self.ago(unit, size).start, size)) + return type(self)(unit, self.ago(unit, size).start, size) def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: """Increment (or decrement) the given period with offset units. @@ -570,19 +502,19 @@ def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: >>> start = Instant(2014, 2, 3) - >>> Period((DAY, start, 1)).offset("first-of", MONTH) - Period((day, Instant(year=2014, month=2, day=1), 1)) + >>> Period(DAY, start, 1).offset("first-of", MONTH) + Period(unit=day, start=Instant(year=2014, month=2, day=1), size=1) - >>> Period((MONTH, start, 4)).offset("last-of", MONTH) - Period((month, Instant(year=2014, month=2, day=28), 4)) + >>> Period(MONTH, start, 4).offset("last-of", MONTH) + Period(unit=month, start=Instant(year=2014, month=2, day=28), size=4) >>> start = Instant(2021, 1, 1) - >>> Period((DAY, start, 365)).offset(-3) - Period((day, Instant(year=2020, month=12, day=29), 365)) + >>> Period(DAY, start, 365).offset(-3) + Period(unit=day, start=Instant(year=2020, month=12, day=29), size=365) - >>> Period((DAY, start, 365)).offset(1, YEAR) - Period((day, Instant(year=2022, month=1, day=1), 365)) + >>> Period(DAY, start, 365).offset(1, YEAR) + Period(unit=day, start=Instant(year=2022, month=1, day=1), size=365) """ @@ -591,7 +523,7 @@ def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: start = self.start.offset(offset, unit) - return type(self)((self.unit, start, self.size)) + return type(self)(self.unit, start, self.size) def subperiods(self, unit: DateUnit) -> Sequence[Period]: """Return the list of all the periods of unit ``unit``. @@ -611,13 +543,13 @@ def subperiods(self, unit: DateUnit) -> Sequence[Period]: >>> start = Instant(2021, 1, 1) - >>> period = Period((YEAR, start, 1)) + >>> period = Period(YEAR, start, 1) >>> period.subperiods(MONTH) - [Period((month, Instant(year=2021, month=1, day=1), 1)),...1), 1))] + [Period(unit=month, start=Instant(year=2021, month=1, day=1), size=1), ...] - >>> period = Period((YEAR, start, 2)) + >>> period = Period(YEAR, start, 2) >>> period.subperiods(YEAR) - [Period((year, Instant(year=2021, month=1, day=1), 1)), ...1), 1))] + [Period(unit=year, start=Instant(year=2021, month=1, day=1), size=1), ...] .. versionchanged:: 39.0.0: Renamed from ``get_subperiods`` to ``subperiods``. diff --git a/openfisca_core/periods/tests/test_builders.py b/openfisca_core/periods/tests/test_builders.py index f275b488e5..553daf307c 100644 --- a/openfisca_core/periods/tests/test_builders.py +++ b/openfisca_core/periods/tests/test_builders.py @@ -43,7 +43,7 @@ def test_build_instant(arg, expected): [None, TypeError], [eternity, ValueError], [year, ValueError], - [Period((day, Instant(1, 1, 1), 365)), ValueError], + [Period(day, Instant(1, 1, 1), 365), ValueError], ]) def test_build_instant_with_an_invalid_argument(arg, error): """Raises ``ValueError`` when given an invalid argument.""" @@ -53,33 +53,33 @@ def test_build_instant_with_an_invalid_argument(arg, error): @pytest.mark.parametrize("arg, expected", [ - ["1000", Period((year, Instant(1000, 1, 1), 1))], - ["1000-01", Period((month, Instant(1000, 1, 1), 1))], - ["1000-01-01", Period((day, Instant(1000, 1, 1), 1))], - ["1004-02-29", Period((day, Instant(1004, 2, 29), 1))], - ["ETERNITY", Period((eternity, Instant(1, 1, 1), 1))], - ["day:1000-01-01", Period((day, Instant(1000, 1, 1), 1))], - ["day:1000-01-01:3", Period((day, Instant(1000, 1, 1), 3))], - ["eternity", Period((eternity, Instant(1, 1, 1), 1))], - ["month:1000-01", Period((month, Instant(1000, 1, 1), 1))], - ["month:1000-01-01", Period((month, Instant(1000, 1, 1), 1))], - ["month:1000-01-01:3", Period((month, Instant(1000, 1, 1), 3))], - ["month:1000-01:3", Period((month, Instant(1000, 1, 1), 3))], - ["year:1000", Period((year, Instant(1000, 1, 1), 1))], - ["year:1000-01", Period((year, Instant(1000, 1, 1), 1))], - ["year:1000-01-01", Period((year, Instant(1000, 1, 1), 1))], - ["year:1000-01-01:3", Period((year, Instant(1000, 1, 1), 3))], - ["year:1000-01:3", Period((year, Instant(1000, 1, 1), 3))], - ["year:1000:3", Period((year, Instant(1000, 1, 1), 3))], - [1000, Period((year, Instant(1000, 1, 1), 1))], - [eternity, Period((eternity, Instant(1, 1, 1), 1))], - [Instant(1, 1, 1), Period((day, Instant(1, 1, 1), 1))], - [Period((day, Instant(1, 1, 1), 365)), Period((day, Instant(1, 1, 1), 365))], - ["month:1000:1", Period((2, Instant(1000, 1, 1), 1))], - ["month:1000", Period((2, Instant(1000, 1, 1), 1))], - ["day:1000:1", Period((1, Instant(1000, 1, 1), 1))], - ["day:1000-01:1", Period((1, Instant(1000, 1, 1), 1))], - ["day:1000-01", Period((1, Instant(1000, 1, 1), 1))], + ["1000", Period(year, Instant(1000, 1, 1), 1)], + ["1000-01", Period(month, Instant(1000, 1, 1), 1)], + ["1000-01-01", Period(day, Instant(1000, 1, 1), 1)], + ["1004-02-29", Period(day, Instant(1004, 2, 29), 1)], + ["ETERNITY", Period(eternity, Instant(1, 1, 1), 1)], + ["day:1000-01-01", Period(day, Instant(1000, 1, 1), 1)], + ["day:1000-01-01:3", Period(day, Instant(1000, 1, 1), 3)], + ["eternity", Period(eternity, Instant(1, 1, 1), 1)], + ["month:1000-01", Period(month, Instant(1000, 1, 1), 1)], + ["month:1000-01-01", Period(month, Instant(1000, 1, 1), 1)], + ["month:1000-01-01:3", Period(month, Instant(1000, 1, 1), 3)], + ["month:1000-01:3", Period(month, Instant(1000, 1, 1), 3)], + ["year:1000", Period(year, Instant(1000, 1, 1), 1)], + ["year:1000-01", Period(year, Instant(1000, 1, 1), 1)], + ["year:1000-01-01", Period(year, Instant(1000, 1, 1), 1)], + ["year:1000-01-01:3", Period(year, Instant(1000, 1, 1), 3)], + ["year:1000-01:3", Period(year, Instant(1000, 1, 1), 3)], + ["year:1000:3", Period(year, Instant(1000, 1, 1), 3)], + [1000, Period(year, Instant(1000, 1, 1), 1)], + [eternity, Period(eternity, Instant(1, 1, 1), 1)], + [Instant(1, 1, 1), Period(day, Instant(1, 1, 1), 1)], + [Period(day, Instant(1, 1, 1), 365), Period(day, Instant(1, 1, 1), 365)], + ["month:1000:1", Period(2, Instant(1000, 1, 1), 1)], + ["month:1000", Period(2, Instant(1000, 1, 1), 1)], + ["day:1000:1", Period(1, Instant(1000, 1, 1), 1)], + ["day:1000-01:1", Period(1, Instant(1000, 1, 1), 1)], + ["day:1000-01", Period(1, Instant(1000, 1, 1), 1)], ]) def test_build_period(arg, expected): """Returns the expected ``Period``.""" diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index be7d85e86d..01004d822b 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -23,7 +23,7 @@ def instant(): def test_str_with_years(date_unit, instant, size, expected): """Returns the expected string.""" - assert str(Period((date_unit, instant, size))) == expected + assert str(Period(date_unit, instant, size)) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ @@ -34,7 +34,7 @@ def test_str_with_years(date_unit, instant, size, expected): def test_str_with_months(date_unit, instant, size, expected): """Returns the expected string.""" - assert str(Period((date_unit, instant, size))) == expected + assert str(Period(date_unit, instant, size)) == expected @pytest.mark.parametrize("date_unit, instant, size, expected", [ @@ -45,7 +45,7 @@ def test_str_with_months(date_unit, instant, size, expected): def test_str_with_days(date_unit, instant, size, expected): """Returns the expected string.""" - assert str(Period((date_unit, instant, size))) == expected + assert str(Period(date_unit, instant, size)) == expected @pytest.mark.parametrize("period_unit, unit, start, cease, count", [ @@ -59,41 +59,41 @@ def test_str_with_days(date_unit, instant, size, expected): def test_subperiods(instant, period_unit, unit, start, cease, count): """Returns the expected subperiods.""" - period = Period((period_unit, instant, 3)) + period = Period(period_unit, instant, 3) subperiods = period.subperiods(unit) assert len(subperiods) == count - assert subperiods[0] == Period((unit, start, 1)) - assert subperiods[-1] == Period((unit, cease, 1)) + assert subperiods[0] == Period(unit, start, 1) + assert subperiods[-1] == Period(unit, cease, 1) @pytest.mark.parametrize("period_unit, offset, unit, expected", [ - [day, "first-of", month, Period((day, Instant(2022, 12, 1), 3))], - [day, "first-of", year, Period((day, Instant(2022, 1, 1), 3))], - [day, "last-of", month, Period((day, Instant(2022, 12, 31), 3))], - [day, "last-of", year, Period((day, Instant(2022, 12, 31), 3))], - [day, -3, year, Period((day, Instant(2019, 12, 31), 3))], - [day, 1, month, Period((day, Instant(2023, 1, 31), 3))], - [day, 3, day, Period((day, Instant(2023, 1, 3), 3))], - [month, "first-of", month, Period((month, Instant(2022, 12, 1), 3))], - [month, "first-of", year, Period((month, Instant(2022, 1, 1), 3))], - [month, "last-of", month, Period((month, Instant(2022, 12, 31), 3))], - [month, "last-of", year, Period((month, Instant(2022, 12, 31), 3))], - [month, -3, year, Period((month, Instant(2019, 12, 31), 3))], - [month, 1, month, Period((month, Instant(2023, 1, 31), 3))], - [month, 3, day, Period((month, Instant(2023, 1, 3), 3))], - [year, "first-of", month, Period((year, Instant(2022, 12, 1), 3))], - [year, "first-of", year, Period((year, Instant(2022, 1, 1), 3))], - [year, "last-of", month, Period((year, Instant(2022, 12, 31), 3))], - [year, "last-of", year, Period((year, Instant(2022, 12, 31), 3))], - [year, -3, year, Period((year, Instant(2019, 12, 31), 3))], - [year, 1, month, Period((year, Instant(2023, 1, 31), 3))], - [year, 3, day, Period((year, Instant(2023, 1, 3), 3))], + [day, "first-of", month, Period(day, Instant(2022, 12, 1), 3)], + [day, "first-of", year, Period(day, Instant(2022, 1, 1), 3)], + [day, "last-of", month, Period(day, Instant(2022, 12, 31), 3)], + [day, "last-of", year, Period(day, Instant(2022, 12, 31), 3)], + [day, -3, year, Period(day, Instant(2019, 12, 31), 3)], + [day, 1, month, Period(day, Instant(2023, 1, 31), 3)], + [day, 3, day, Period(day, Instant(2023, 1, 3), 3)], + [month, "first-of", month, Period(month, Instant(2022, 12, 1), 3)], + [month, "first-of", year, Period(month, Instant(2022, 1, 1), 3)], + [month, "last-of", month, Period(month, Instant(2022, 12, 31), 3)], + [month, "last-of", year, Period(month, Instant(2022, 12, 31), 3)], + [month, -3, year, Period(month, Instant(2019, 12, 31), 3)], + [month, 1, month, Period(month, Instant(2023, 1, 31), 3)], + [month, 3, day, Period(month, Instant(2023, 1, 3), 3)], + [year, "first-of", month, Period(year, Instant(2022, 12, 1), 3)], + [year, "first-of", year, Period(year, Instant(2022, 1, 1), 3)], + [year, "last-of", month, Period(year, Instant(2022, 12, 31), 3)], + [year, "last-of", year, Period(year, Instant(2022, 12, 31), 3)], + [year, -3, year, Period(year, Instant(2019, 12, 31), 3)], + [year, 1, month, Period(year, Instant(2023, 1, 31), 3)], + [year, 3, day, Period(year, Instant(2023, 1, 3), 3)], ]) def test_offset(instant, period_unit, offset, unit, expected): """Returns the expected ``Period``.""" - period = Period((period_unit, instant, 3)) + period = Period(period_unit, instant, 3) assert period.offset(offset, unit) == expected @@ -110,7 +110,7 @@ def test_offset(instant, period_unit, offset, unit, expected): def test_day_size_in_months(date_unit, instant, size, expected): """Returns the expected number of months.""" - period = Period((date_unit, instant, size)) + period = Period(date_unit, instant, size) assert period.count(month) == expected @@ -129,6 +129,6 @@ def test_day_size_in_months(date_unit, instant, size, expected): def test_day_size_in_days(date_unit, instant, size, expected): """Returns the expected number of days.""" - period = Period((date_unit, instant, size)) + period = Period(date_unit, instant, size) assert period.count(day) == expected diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index 7fe032ef33..62a4dbc2ec 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Tuple, TypeVar +from typing import Any, Iterator, Tuple, TypeVar from typing_extensions import Protocol import abc @@ -20,6 +20,10 @@ class Offsetable(Protocol[T, U, V]): def __init__(self, *args: Tuple[T, U, V]) -> None: ... + @abc.abstractmethod + def __iter__(self) -> Iterator[U]: + ... + @abc.abstractmethod def __le__(self, other: Any) -> bool: ... From 574cf5fe0b7d8c3edbdc09f7ee006ef57fbc2eeb Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 22 Dec 2022 23:27:07 +0100 Subject: [PATCH 89/93] Fix offsetable type --- openfisca_core/periods/instant_.py | 10 +++--- openfisca_core/periods/period_.py | 36 ++++++++++--------- openfisca_core/periods/typing.py | 23 +++++++----- .../taxbenefitsystems/tax_benefit_system.py | 6 ++-- 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 784419f867..1e75a69f31 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -159,21 +159,21 @@ def offset(self, offset: str | int, unit: DateUnit) -> Instant: return self if offset == "first-of" and unit == MONTH: - return type(self)(year, month, 1) + return type(self)(year = year, month = month, day = 1) if offset == "first-of" and unit == YEAR: - return type(self)(year, 1, 1) + return type(self)(year = year, month = 1, day = 1) if offset == "last-of" and unit == MONTH: day = calendar.monthrange(year, month)[1] - return type(self)(year, month, day) + return type(self)(year = year, month = month, day = day) if offset == "last-of" and unit == YEAR: - return type(self)(year, 12, 31) + return type(self)(year = year, month = 12, day = 31) if not isinstance(offset, int): raise OffsetTypeError(offset) date = self.add(unit.plural, offset) - return type(self)(date.year, date.month, date.day) + return type(self)(year = date.year, month = date.month, day = date.day) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 1b2fa159a2..2b65c79b16 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -6,12 +6,10 @@ from ._dates import DateUnit from ._errors import DateUnitValueError -from .typing import Offsetable +from .typing import Instant DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) -Instant = Offsetable[int, int, int] - class Period(NamedTuple): """Toolbox to handle date intervals. @@ -67,7 +65,7 @@ class Period(NamedTuple): unit: DateUnit #: The "instant" the Period starts at. - start: Offsetable[int, int, int] + start: Instant #: The amount of ``unit``, starting at ``start``, at least ``1``. size: int @@ -104,10 +102,6 @@ def __str__(self) -> str: """ - year: int - month: int - day: int - unit, (year, month, day), size = self if unit == ETERNITY: @@ -193,7 +187,7 @@ def stop(self) -> Instant: unit, start, size = self if unit == ETERNITY: - return type(self.start)((1, 1, 1)) + return type(self.start)(1, 1, 1) return start.offset(size, unit).offset(-1, DAY) @@ -321,7 +315,9 @@ def first(self, unit: DateUnit) -> Period: """ - return type(self)(unit, self.start.offset("first-of", unit), 1) + start: Instant = self.start.offset("first-of", unit) + + return type(self)(unit = unit, start = start, size = 1) def come(self, unit: DateUnit, size: int = 1) -> Period: """The next ``unit``s ``size`` from ``Period.start``. @@ -362,7 +358,9 @@ def come(self, unit: DateUnit, size: int = 1) -> Period: """ - return type(self)(unit, self.first(unit).start, 1).offset(size) + start: Instant = self.first(unit).start + + return type(self)(unit = unit, start = start, size = 1).offset(size) def ago(self, unit: DateUnit, size: int = 1) -> Period: """``size`` ``unit``s ago from ``Period.start``. @@ -403,7 +401,9 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: """ - return type(self)(unit, self.start, 1).offset(-size) + start: Instant = self.start + + return type(self)(unit = unit, start = start, size = 1).offset(-size) def until(self, unit: DateUnit, size: int = 1) -> Period: """Next ``unit`` ``size``s from ``Period.start``. @@ -444,7 +444,9 @@ def until(self, unit: DateUnit, size: int = 1) -> Period: """ - return type(self)(unit, self.first(unit).start, size) + start: Instant = self.first(unit).start + + return type(self)(unit = unit, start = start, size = size) def last(self, unit: DateUnit, size: int = 1) -> Period: """Last ``size`` ``unit``s from ``Period.start``. @@ -485,7 +487,9 @@ def last(self, unit: DateUnit, size: int = 1) -> Period: """ - return type(self)(unit, self.ago(unit, size).start, size) + start: Instant = self.ago(unit, size).start + + return type(self)(unit = unit, start = start, size = size) def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: """Increment (or decrement) the given period with offset units. @@ -521,9 +525,9 @@ def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: if unit is None: unit = self.unit - start = self.start.offset(offset, unit) + start: Instant = self.start.offset(offset, unit) - return type(self)(self.unit, start, self.size) + return type(self)(unit = self.unit, start = start, size = self.size) def subperiods(self, unit: DateUnit) -> Sequence[Period]: """Return the list of all the periods of unit ``unit``. diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index 62a4dbc2ec..9f31a7eaa3 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -2,26 +2,26 @@ from __future__ import annotations -from typing import Any, Iterator, Tuple, TypeVar +from typing import Any, Iterator, TypeVar from typing_extensions import Protocol import abc from pendulum.datetime import Date -Self = TypeVar("Self") -T = TypeVar("T", covariant = True) -U = TypeVar("U", covariant = True) -V = TypeVar("V", covariant = True) +_T = TypeVar("_T", covariant = True) +_U = TypeVar("_U", covariant = True) +_V = TypeVar("_V", covariant = True) +_Self = TypeVar("_Self") -class Offsetable(Protocol[T, U, V]): +class _Offsetable(Protocol[_T, _U, _V]): @abc.abstractmethod - def __init__(self, *args: Tuple[T, U, V]) -> None: + def __init__(self, *args: _T | _U | _V) -> None: ... @abc.abstractmethod - def __iter__(self) -> Iterator[U]: + def __iter__(self) -> Iterator[_T | _U | _V]: ... @abc.abstractmethod @@ -56,5 +56,10 @@ def date(self) -> Date: ... @abc.abstractmethod - def offset(self: Self, offset: Any, unit: Any) -> Self: + def offset(self: _Self, offset: Any, unit: Any) -> _Self: ... + + +Instant = _Offsetable[int, int, int] + +Period = _Offsetable[Any, Instant, int] diff --git a/openfisca_core/taxbenefitsystems/tax_benefit_system.py b/openfisca_core/taxbenefitsystems/tax_benefit_system.py index 944f649f16..a21c96e0a1 100644 --- a/openfisca_core/taxbenefitsystems/tax_benefit_system.py +++ b/openfisca_core/taxbenefitsystems/tax_benefit_system.py @@ -25,7 +25,7 @@ from openfisca_core.populations import GroupPopulation, Population from openfisca_core.simulations import SimulationBuilder from openfisca_core.variables import Variable -from openfisca_core.periods.typing import Offsetable +from openfisca_core.periods.typing import Instant log = logging.getLogger(__name__) @@ -388,7 +388,7 @@ def _get_baseline_parameters_at_instant(self, instant): @functools.lru_cache() def get_parameters_at_instant( self, - instant: periods.Period | Offsetable[int, int, int] | str | int, + instant: periods.Period | Instant | str | int, ) -> Optional[types.ParameterNodeAtInstant]: """Get the parameters of the legislation at a given instant @@ -400,7 +400,7 @@ def get_parameters_at_instant( """ - key: Offsetable[int, int, int] + key: Instant if isinstance(instant, periods.Instant): key = instant From 4c168df9a93e488a92b0dc18d90f9faf0956b8f1 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 23 Dec 2022 11:16:49 +0100 Subject: [PATCH 90/93] Make Instant._add private --- openfisca_core/periods/__init__.py | 1 + openfisca_core/periods/instant_.py | 52 +++++++++++++++--------------- openfisca_core/periods/typing.py | 6 ++-- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index c293f51e11..0314d18b6e 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -35,6 +35,7 @@ from .period_ import Period DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) +day, month, year, eternity = tuple(DateUnit) # Deprecated diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 1e75a69f31..52280de4bd 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -93,31 +93,6 @@ def date(self) -> Date: return pendulum.date(*self) - def add(self, unit: str, count: int) -> Date: - """Add ``count`` ``unit``s to a ``date``. - - Args: - unit: The unit to add. - count: The number of units to add. - - Returns: - A new Date. - - Examples: - >>> instant = Instant(2021, 10, 1) - >>> instant.add("months", 6) - Date(2022, 4, 1) - - .. versionadded:: 39.0.0 - - """ - - fun: Callable[..., Date] = self.date().add - - new: Date = fun(**{unit: count}) - - return new - def offset(self, offset: str | int, unit: DateUnit) -> Instant: """Increments/decrements the given instant with offset units. @@ -174,6 +149,31 @@ def offset(self, offset: str | int, unit: DateUnit) -> Instant: if not isinstance(offset, int): raise OffsetTypeError(offset) - date = self.add(unit.plural, offset) + date = self._add(unit.plural, offset) return type(self)(year = date.year, month = date.month, day = date.day) + + def _add(self, unit: str, count: int) -> Date: + """Add ``count`` ``unit``s to a ``date``. + + Args: + unit: The unit to add. + count: The number of units to add. + + Returns: + A new Date. + + Examples: + >>> instant = Instant(2021, 10, 1) + >>> instant._add("months", 6) + Date(2022, 4, 1) + + .. versionadded:: 39.0.0 + + """ + + fun: Callable[..., Date] = self.date().add + + new: Date = fun(**{unit: count}) + + return new diff --git a/openfisca_core/periods/typing.py b/openfisca_core/periods/typing.py index 9f31a7eaa3..7c3fa467f1 100644 --- a/openfisca_core/periods/typing.py +++ b/openfisca_core/periods/typing.py @@ -48,15 +48,15 @@ def day(self) -> int: ... @abc.abstractmethod - def add(self, unit: str, count: int) -> Date: + def date(self) -> Date: ... @abc.abstractmethod - def date(self) -> Date: + def offset(self: _Self, offset: Any, unit: Any) -> _Self: ... @abc.abstractmethod - def offset(self: _Self, offset: Any, unit: Any) -> _Self: + def _add(self, unit: str, count: int) -> Date: ... From d3b24df64bd339a3b10f3e0b491dc7a784df070e Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 23 Dec 2022 11:33:50 +0100 Subject: [PATCH 91/93] Refactor Instant.offset --- openfisca_core/periods/instant_.py | 36 +++++++++++++----------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/openfisca_core/periods/instant_.py b/openfisca_core/periods/instant_.py index 52280de4bd..8e90640613 100644 --- a/openfisca_core/periods/instant_.py +++ b/openfisca_core/periods/instant_.py @@ -11,7 +11,7 @@ from ._dates import DateUnit from ._errors import DateUnitValueError, OffsetTypeError -DAY, MONTH, YEAR, _ = tuple(DateUnit) +day, month, year, _ = tuple(DateUnit) class Instant(NamedTuple): @@ -60,8 +60,6 @@ class Instant(NamedTuple): >>> instant > (2020, 9, 13) True - >>> year, month, day = instant - """ #: The year. @@ -108,50 +106,48 @@ def offset(self, offset: str | int, unit: DateUnit) -> Instant: OffsetTypeError: When ``offset`` is of type ``int``. Examples: - >>> Instant(2020, 12, 31).offset("first-of", MONTH) + >>> Instant(2020, 12, 31).offset("first-of", month) Instant(year=2020, month=12, day=1) - >>> Instant(2020, 1, 1).offset("last-of", YEAR) + >>> Instant(2020, 1, 1).offset("last-of", year) Instant(year=2020, month=12, day=31) - >>> Instant(2020, 1, 1).offset(1, YEAR) + >>> Instant(2020, 1, 1).offset(1, year) Instant(year=2021, month=1, day=1) - >>> Instant(2020, 1, 1).offset(-3, DAY) + >>> Instant(2020, 1, 1).offset(-3, day) Instant(year=2019, month=12, day=29) """ - year, month, day = self - if not isinstance(unit, DateUnit): raise DateUnitValueError(unit) if not unit & DateUnit.isoformat: raise DateUnitValueError(unit) - if offset in {"first-of", "last-of"} and unit == DAY: + if offset in {"first-of", "last-of"} and unit == day: return self - if offset == "first-of" and unit == MONTH: - return type(self)(year = year, month = month, day = 1) + if offset == "first-of" and unit == month: + return Instant(self.year, self.month, 1) - if offset == "first-of" and unit == YEAR: - return type(self)(year = year, month = 1, day = 1) + if offset == "first-of" and unit == year: + return Instant(self.year, 1, 1) - if offset == "last-of" and unit == MONTH: - day = calendar.monthrange(year, month)[1] - return type(self)(year = year, month = month, day = day) + if offset == "last-of" and unit == month: + monthrange = calendar.monthrange(self.year, self.month) + return Instant(self.year, self.month, monthrange[1]) - if offset == "last-of" and unit == YEAR: - return type(self)(year = year, month = 12, day = 31) + if offset == "last-of" and unit == year: + return Instant(self.year, 12, 31) if not isinstance(offset, int): raise OffsetTypeError(offset) date = self._add(unit.plural, offset) - return type(self)(year = date.year, month = date.month, day = date.day) + return Instant(date.year, date.month, date.day) def _add(self, unit: str, count: int) -> Date: """Add ``count`` ``unit``s to a ``date``. From 30e416461a1ecd0932dd7e95d816fff0ceb865b8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 23 Dec 2022 12:14:15 +0100 Subject: [PATCH 92/93] Make Period.__str__ less surprising --- openfisca_core/periods/period_.py | 207 +++++++++----------- openfisca_core/periods/tests/test_period.py | 6 +- 2 files changed, 99 insertions(+), 114 deletions(-) diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index 2b65c79b16..a42570d64f 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -8,7 +8,7 @@ from ._errors import DateUnitValueError from .typing import Instant -DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) +day, month, year, eternity = tuple(DateUnit) class Period(NamedTuple): @@ -18,7 +18,7 @@ class Period(NamedTuple): >>> from openfisca_core.periods import Instant >>> start = Instant(2021, 9, 1) - >>> period = Period(YEAR, start, 3) + >>> period = Period(year, start, 3) ``Periods`` are represented as a ``tuple`` containing the ``unit``, an ``Instant`` and the ``size``: @@ -49,14 +49,12 @@ class Period(NamedTuple): >>> len(period) 3 - >>> period == Period(YEAR, start, 3) + >>> period == Period(year, start, 3) True - >>> period > Period(YEAR, start, 3) + >>> period > Period(year, start, 3) False - >>> unit, (year, month, day), size = period - Since a period is a triple it can be used as a dictionary key. """ @@ -80,60 +78,49 @@ def __str__(self) -> str: >>> from openfisca_core.periods import Instant >>> jan = Instant(2021, 1, 1) - >>> feb = jan.offset(1, MONTH) + >>> feb = jan.offset(1, month) - >>> str(Period(YEAR, jan, 1)) + >>> str(Period(year, jan, 1)) '2021' - >>> str(Period(YEAR, feb, 1)) - 'year:2021-02' + >>> str(Period(year, feb, 1)) + 'year:2021-02:1' - >>> str(Period(MONTH, feb, 1)) + >>> str(Period(month, feb, 1)) '2021-02' - >>> str(Period(YEAR, jan, 2)) + >>> str(Period(year, jan, 2)) 'year:2021:2' - >>> str(Period(MONTH, jan, 2)) + >>> str(Period(month, jan, 2)) 'month:2021-01:2' - >>> str(Period(MONTH, jan, 12)) - '2021' + >>> str(Period(month, jan, 12)) + 'month:2021-01:12' """ - unit, (year, month, day), size = self - - if unit == ETERNITY: - return str(unit.name) + if self.unit == eternity: + return str(self.unit.name) - # 1 year long period - if unit == MONTH and size == 12 or unit == YEAR and size == 1: - if month == 1: - # civil year starting from january - return str(year) + string = f"{self.start.year:04d}" - else: - # rolling year - return f"{str(YEAR)}:{year}-{month:02d}" + if self.unit == year and self.start.month > 1: + string = f"{string}-{self.start.month:02d}" - # simple month - if unit == MONTH and size == 1: - return f"{year}-{month:02d}" + if self.unit < year: + string = f"{string}-{self.start.month:02d}" - # several civil years - if unit == YEAR and month == 1: - return f"{str(unit)}:{year}:{size}" + if self.unit < month: + string = f"{string}-{self.start.day:02d}" - if unit == DAY: - if size == 1: - return f"{year}-{month:02d}-{day:02d}" + if self.unit == year and self.start.month > 1: + return f"{str(self.unit)}:{string}:{self.size}" - else: - return f"{str(unit)}:{year}-{month:02d}-{day:02d}:{size}" + if self.size > 1: + return f"{str(self.unit)}:{string}:{self.size}" - # complex period - return f"{str(unit)}:{year}-{month:02d}:{size}" + return string def __contains__(self, other: object) -> bool: """Checks if a ``period`` contains another one. @@ -148,8 +135,8 @@ def __contains__(self, other: object) -> bool: >>> from openfisca_core.periods import Instant >>> start = Instant(2021, 1, 1) - >>> period = Period(YEAR, start, 1) - >>> sub_period = Period(MONTH, start, 3) + >>> period = Period(year, start, 1) + >>> sub_period = Period(month, start, 3) >>> sub_period in period True @@ -173,23 +160,21 @@ def stop(self) -> Instant: >>> start = Instant(2012, 2, 29) - >>> Period(YEAR, start, 2).stop + >>> Period(year, start, 2).stop Instant(year=2014, month=2, day=27) - >>> Period(MONTH, start, 36).stop + >>> Period(month, start, 36).stop Instant(year=2015, month=2, day=27) - >>> Period(DAY, start, 1096).stop + >>> Period(day, start, 1096).stop Instant(year=2015, month=2, day=28) """ - unit, start, size = self - - if unit == ETERNITY: + if self.unit == eternity: return type(self.start)(1, 1, 1) - return start.offset(size, unit).offset(-1, DAY) + return self.start.offset(self.size, self.unit).offset(-1, day) def date(self) -> datetime.date: """The date representation of the ``period``'s' start date. @@ -205,11 +190,11 @@ def date(self) -> datetime.date: >>> start = Instant(2021, 10, 1) - >>> period = Period(YEAR, start, 1) + >>> period = Period(year, start, 1) >>> period.date() Date(2021, 10, 1) - >>> period = Period(YEAR, start, 3) + >>> period = Period(year, start, 3) >>> period.date() Traceback (most recent call last): ValueError: 'date' undefined for period size > 1: year:2021-10:3. @@ -241,29 +226,29 @@ def count(self, unit: DateUnit) -> int: >>> start = Instant(2021, 10, 1) - >>> period = Period(YEAR, start, 3) - >>> period.count(DAY) + >>> period = Period(year, start, 3) + >>> period.count(day) 1096 - >>> period = Period(MONTH, start, 3) - >>> period.count(DAY) + >>> period = Period(month, start, 3) + >>> period.count(day) 92 - >>> period = Period(YEAR, start, 3) - >>> period.count(MONTH) + >>> period = Period(year, start, 3) + >>> period.count(month) 36 - >>> period = Period(DAY, start, 3) - >>> period.count(MONTH) + >>> period = Period(day, start, 3) + >>> period.count(month) Traceback (most recent call last): ValueError: Cannot calculate number of months in a day. - >>> period = Period(YEAR, start, 3) - >>> period.count(YEAR) + >>> period = Period(year, start, 3) + >>> period.count(year) 3 - >>> period = Period(MONTH, start, 3) - >>> period.count(YEAR) + >>> period = Period(month, start, 3) + >>> period.count(year) Traceback (most recent call last): ValueError: Cannot calculate number of years in a month. @@ -274,11 +259,11 @@ def count(self, unit: DateUnit) -> int: if unit == self.unit: return self.size - if unit == DAY and self.unit in {MONTH, YEAR}: + if unit == day and self.unit in {month, year}: delta: int = (self.stop.date() - self.start.date()).days return delta + 1 - if unit == MONTH and self.unit == YEAR: + if unit == month and self.unit == year: return self.size * 12 raise ValueError( @@ -300,15 +285,15 @@ def first(self, unit: DateUnit) -> Period: >>> start = Instant(2023, 1, 1) - >>> period = Period(YEAR, start, 3) + >>> period = Period(year, start, 3) - >>> period.first(DAY) + >>> period.first(day) Period(unit=day, start=Instant(year=2023, month=1, day=1), size=1) - >>> period.first(MONTH) + >>> period.first(month) Period(unit=month, start=Instant(year=2023, month=1, day=1), size=1) - >>> period.first(YEAR) + >>> period.first(year) Period(unit=year, start=Instant(year=2023, month=1, day=1), size=1) .. versionadded:: 39.0.0 @@ -317,7 +302,7 @@ def first(self, unit: DateUnit) -> Period: start: Instant = self.start.offset("first-of", unit) - return type(self)(unit = unit, start = start, size = 1) + return Period(unit, start, 1) def come(self, unit: DateUnit, size: int = 1) -> Period: """The next ``unit``s ``size`` from ``Period.start``. @@ -334,24 +319,24 @@ def come(self, unit: DateUnit, size: int = 1) -> Period: >>> start = Instant(2023, 1, 1) - >>> period = Period(YEAR, start, 3) + >>> period = Period(year, start, 3) - >>> period.come(DAY) + >>> period.come(day) Period(unit=day, start=Instant(year=2023, month=1, day=2), size=1) - >>> period.come(DAY, 7) + >>> period.come(day, 7) Period(unit=day, start=Instant(year=2023, month=1, day=8), size=1) - >>> period.come(MONTH) + >>> period.come(month) Period(unit=month, start=Instant(year=2023, month=2, day=1), size=1) - >>> period.come(MONTH, 3) + >>> period.come(month, 3) Period(unit=month, start=Instant(year=2023, month=4, day=1), size=1) - >>> period.come(YEAR) + >>> period.come(year) Period(unit=year, start=Instant(year=2024, month=1, day=1), size=1) - >>> period.come(YEAR, 1) + >>> period.come(year, 1) Period(unit=year, start=Instant(year=2024, month=1, day=1), size=1) .. versionadded:: 39.0.0 @@ -360,7 +345,7 @@ def come(self, unit: DateUnit, size: int = 1) -> Period: start: Instant = self.first(unit).start - return type(self)(unit = unit, start = start, size = 1).offset(size) + return Period(unit, start, 1).offset(size) def ago(self, unit: DateUnit, size: int = 1) -> Period: """``size`` ``unit``s ago from ``Period.start``. @@ -377,24 +362,24 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: >>> start = Instant(2020, 3, 31) - >>> period = Period(YEAR, start, 3) + >>> period = Period(year, start, 3) - >>> period.ago(DAY) + >>> period.ago(day) Period(unit=day, start=Instant(year=2020, month=3, day=30), size=1) - >>> period.ago(DAY, 7) + >>> period.ago(day, 7) Period(unit=day, start=Instant(year=2020, month=3, day=24), size=1) - >>> period.ago(MONTH) + >>> period.ago(month) Period(unit=month, start=Instant(year=2020, month=2, day=29), size=1) - >>> period.ago(MONTH, 3) + >>> period.ago(month, 3) Period(unit=month, start=Instant(year=2019, month=12, day=31), size=1) - >>> period.ago(YEAR) + >>> period.ago(year) Period(unit=year, start=Instant(year=2019, month=3, day=31), size=1) - >>> period.ago(YEAR, 1) + >>> period.ago(year, 1) Period(unit=year, start=Instant(year=2019, month=3, day=31), size=1) .. versionadded:: 39.0.0 @@ -403,7 +388,7 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: start: Instant = self.start - return type(self)(unit = unit, start = start, size = 1).offset(-size) + return Period(unit, start, 1).offset(-size) def until(self, unit: DateUnit, size: int = 1) -> Period: """Next ``unit`` ``size``s from ``Period.start``. @@ -420,24 +405,24 @@ def until(self, unit: DateUnit, size: int = 1) -> Period: >>> start = Instant(2023, 1, 1) - >>> period = Period(YEAR, start, 3) + >>> period = Period(year, start, 3) - >>> period.until(DAY) + >>> period.until(day) Period(unit=day, start=Instant(year=2023, month=1, day=1), size=1) - >>> period.until(DAY, 7) + >>> period.until(day, 7) Period(unit=day, start=Instant(year=2023, month=1, day=1), size=7) - >>> period.until(MONTH) + >>> period.until(month) Period(unit=month, start=Instant(year=2023, month=1, day=1), size=1) - >>> period.until(MONTH, 3) + >>> period.until(month, 3) Period(unit=month, start=Instant(year=2023, month=1, day=1), size=3) - >>> period.until(YEAR) + >>> period.until(year) Period(unit=year, start=Instant(year=2023, month=1, day=1), size=1) - >>> period.until(YEAR, 1) + >>> period.until(year, 1) Period(unit=year, start=Instant(year=2023, month=1, day=1), size=1) .. versionadded:: 39.0.0 @@ -446,7 +431,7 @@ def until(self, unit: DateUnit, size: int = 1) -> Period: start: Instant = self.first(unit).start - return type(self)(unit = unit, start = start, size = size) + return Period(unit, start, size) def last(self, unit: DateUnit, size: int = 1) -> Period: """Last ``size`` ``unit``s from ``Period.start``. @@ -463,24 +448,24 @@ def last(self, unit: DateUnit, size: int = 1) -> Period: >>> start = Instant(2023, 1, 1) - >>> period = Period(YEAR, start, 3) + >>> period = Period(year, start, 3) - >>> period.last(DAY) + >>> period.last(day) Period(unit=day, start=Instant(year=2022, month=12, day=31), size=1) - >>> period.last(DAY, 7) + >>> period.last(day, 7) Period(unit=day, start=Instant(year=2022, month=12, day=25), size=7) - >>> period.last(MONTH) + >>> period.last(month) Period(unit=month, start=Instant(year=2022, month=12, day=1), size=1) - >>> period.last(MONTH, 3) + >>> period.last(month, 3) Period(unit=month, start=Instant(year=2022, month=10, day=1), size=3) - >>> period.last(YEAR) + >>> period.last(year) Period(unit=year, start=Instant(year=2022, month=1, day=1), size=1) - >>> period.last(YEAR, 1) + >>> period.last(year, 1) Period(unit=year, start=Instant(year=2022, month=1, day=1), size=1) .. versionadded:: 39.0.0 @@ -489,7 +474,7 @@ def last(self, unit: DateUnit, size: int = 1) -> Period: start: Instant = self.ago(unit, size).start - return type(self)(unit = unit, start = start, size = size) + return Period(unit, start, size) def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: """Increment (or decrement) the given period with offset units. @@ -506,18 +491,18 @@ def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: >>> start = Instant(2014, 2, 3) - >>> Period(DAY, start, 1).offset("first-of", MONTH) + >>> Period(day, start, 1).offset("first-of", month) Period(unit=day, start=Instant(year=2014, month=2, day=1), size=1) - >>> Period(MONTH, start, 4).offset("last-of", MONTH) + >>> Period(month, start, 4).offset("last-of", month) Period(unit=month, start=Instant(year=2014, month=2, day=28), size=4) >>> start = Instant(2021, 1, 1) - >>> Period(DAY, start, 365).offset(-3) + >>> Period(day, start, 365).offset(-3) Period(unit=day, start=Instant(year=2020, month=12, day=29), size=365) - >>> Period(DAY, start, 365).offset(1, YEAR) + >>> Period(day, start, 365).offset(1, year) Period(unit=day, start=Instant(year=2022, month=1, day=1), size=365) """ @@ -527,7 +512,7 @@ def offset(self, offset: str | int, unit: DateUnit | None = None) -> Period: start: Instant = self.start.offset(offset, unit) - return type(self)(unit = self.unit, start = start, size = self.size) + return Period(self.unit, start, self.size) def subperiods(self, unit: DateUnit) -> Sequence[Period]: """Return the list of all the periods of unit ``unit``. @@ -547,12 +532,12 @@ def subperiods(self, unit: DateUnit) -> Sequence[Period]: >>> start = Instant(2021, 1, 1) - >>> period = Period(YEAR, start, 1) - >>> period.subperiods(MONTH) + >>> period = Period(year, start, 1) + >>> period.subperiods(month) [Period(unit=month, start=Instant(year=2021, month=1, day=1), size=1), ...] - >>> period = Period(YEAR, start, 2) - >>> period.subperiods(YEAR) + >>> period = Period(year, start, 2) + >>> period.subperiods(year) [Period(unit=year, start=Instant(year=2021, month=1, day=1), size=1), ...] .. versionchanged:: 39.0.0: diff --git a/openfisca_core/periods/tests/test_period.py b/openfisca_core/periods/tests/test_period.py index 01004d822b..4ca45f8764 100644 --- a/openfisca_core/periods/tests/test_period.py +++ b/openfisca_core/periods/tests/test_period.py @@ -13,12 +13,12 @@ def instant(): @pytest.mark.parametrize("date_unit, instant, size, expected", [ - [month, Instant(2022, 1, 1), 12, "2022"], - [month, Instant(2022, 3, 1), 12, "year:2022-03"], + [month, Instant(2022, 1, 1), 12, "month:2022-01:12"], + [month, Instant(2022, 3, 1), 12, "month:2022-03:12"], [year, Instant(2022, 1, 1), 1, "2022"], [year, Instant(2022, 1, 1), 3, "year:2022:3"], [year, Instant(2022, 1, 3), 3, "year:2022:3"], - [year, Instant(2022, 3, 1), 1, "year:2022-03"], + [year, Instant(2022, 3, 1), 1, "year:2022-03:1"], ]) def test_str_with_years(date_unit, instant, size, expected): """Returns the expected string.""" From 56daa66862940ba96201c581a0198f0d4c70ea6b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Fri, 23 Dec 2022 13:07:10 +0100 Subject: [PATCH 93/93] Harmonise offset syntax --- openfisca_core/periods/__init__.py | 1 + openfisca_core/periods/period_.py | 87 +++++++++++++++--------------- tests/core/test_countries.py | 2 +- tests/core/test_cycles.py | 10 ++-- 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/openfisca_core/periods/__init__.py b/openfisca_core/periods/__init__.py index 0314d18b6e..eb65ff46a8 100644 --- a/openfisca_core/periods/__init__.py +++ b/openfisca_core/periods/__init__.py @@ -36,6 +36,7 @@ DAY, MONTH, YEAR, ETERNITY = tuple(DateUnit) day, month, year, eternity = tuple(DateUnit) +days, months, years, _ = tuple(DateUnit) # Deprecated diff --git a/openfisca_core/periods/period_.py b/openfisca_core/periods/period_.py index a42570d64f..7ff4ec981e 100644 --- a/openfisca_core/periods/period_.py +++ b/openfisca_core/periods/period_.py @@ -9,6 +9,7 @@ from .typing import Instant day, month, year, eternity = tuple(DateUnit) +days, months, years, _ = tuple(DateUnit) class Period(NamedTuple): @@ -227,28 +228,28 @@ def count(self, unit: DateUnit) -> int: >>> start = Instant(2021, 10, 1) >>> period = Period(year, start, 3) - >>> period.count(day) + >>> period.count(days) 1096 >>> period = Period(month, start, 3) - >>> period.count(day) + >>> period.count(days) 92 >>> period = Period(year, start, 3) - >>> period.count(month) + >>> period.count(months) 36 >>> period = Period(day, start, 3) - >>> period.count(month) + >>> period.count(months) Traceback (most recent call last): ValueError: Cannot calculate number of months in a day. >>> period = Period(year, start, 3) - >>> period.count(year) + >>> period.count(years) 3 >>> period = Period(month, start, 3) - >>> period.count(year) + >>> period.count(years) Traceback (most recent call last): ValueError: Cannot calculate number of years in a month. @@ -304,12 +305,12 @@ def first(self, unit: DateUnit) -> Period: return Period(unit, start, 1) - def come(self, unit: DateUnit, size: int = 1) -> Period: + def come(self, size: int, unit: DateUnit) -> Period: """The next ``unit``s ``size`` from ``Period.start``. Args: - unit: The unit of the requested Period. size: The number of units ago. + unit: The unit of the requested Period. Returns: A Period. @@ -321,23 +322,23 @@ def come(self, unit: DateUnit, size: int = 1) -> Period: >>> period = Period(year, start, 3) - >>> period.come(day) + >>> period.come(1, day) Period(unit=day, start=Instant(year=2023, month=1, day=2), size=1) - >>> period.come(day, 7) + >>> period.come(7, days) Period(unit=day, start=Instant(year=2023, month=1, day=8), size=1) - >>> period.come(month) + >>> period.come(1, month) Period(unit=month, start=Instant(year=2023, month=2, day=1), size=1) - >>> period.come(month, 3) + >>> period.come(3, months) Period(unit=month, start=Instant(year=2023, month=4, day=1), size=1) - >>> period.come(year) + >>> period.come(1, year) Period(unit=year, start=Instant(year=2024, month=1, day=1), size=1) - >>> period.come(year, 1) - Period(unit=year, start=Instant(year=2024, month=1, day=1), size=1) + >>> period.come(2, years) + Period(unit=year, start=Instant(year=2025, month=1, day=1), size=1) .. versionadded:: 39.0.0 @@ -347,12 +348,12 @@ def come(self, unit: DateUnit, size: int = 1) -> Period: return Period(unit, start, 1).offset(size) - def ago(self, unit: DateUnit, size: int = 1) -> Period: + def ago(self, size: int, unit: DateUnit) -> Period: """``size`` ``unit``s ago from ``Period.start``. Args: - unit: The unit of the requested Period. size: The number of units ago. + unit: The unit of the requested Period. Returns: A Period. @@ -364,23 +365,23 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: >>> period = Period(year, start, 3) - >>> period.ago(day) + >>> period.ago(1, day) Period(unit=day, start=Instant(year=2020, month=3, day=30), size=1) - >>> period.ago(day, 7) + >>> period.ago(7, days) Period(unit=day, start=Instant(year=2020, month=3, day=24), size=1) - >>> period.ago(month) + >>> period.ago(1, month) Period(unit=month, start=Instant(year=2020, month=2, day=29), size=1) - >>> period.ago(month, 3) + >>> period.ago(3, months) Period(unit=month, start=Instant(year=2019, month=12, day=31), size=1) - >>> period.ago(year) + >>> period.ago(1, year) Period(unit=year, start=Instant(year=2019, month=3, day=31), size=1) - >>> period.ago(year, 1) - Period(unit=year, start=Instant(year=2019, month=3, day=31), size=1) + >>> period.ago(2, years) + Period(unit=year, start=Instant(year=2018, month=3, day=31), size=1) .. versionadded:: 39.0.0 @@ -390,12 +391,12 @@ def ago(self, unit: DateUnit, size: int = 1) -> Period: return Period(unit, start, 1).offset(-size) - def until(self, unit: DateUnit, size: int = 1) -> Period: + def until(self, size: int, unit: DateUnit) -> Period: """Next ``unit`` ``size``s from ``Period.start``. Args: - unit: The unit of the requested Period. size: The number of units to include in the Period. + unit: The unit of the requested Period. Returns: A Period. @@ -407,23 +408,23 @@ def until(self, unit: DateUnit, size: int = 1) -> Period: >>> period = Period(year, start, 3) - >>> period.until(day) + >>> period.until(1, day) Period(unit=day, start=Instant(year=2023, month=1, day=1), size=1) - >>> period.until(day, 7) + >>> period.until(7, days) Period(unit=day, start=Instant(year=2023, month=1, day=1), size=7) - >>> period.until(month) + >>> period.until(1, month) Period(unit=month, start=Instant(year=2023, month=1, day=1), size=1) - >>> period.until(month, 3) + >>> period.until(3, months) Period(unit=month, start=Instant(year=2023, month=1, day=1), size=3) - >>> period.until(year) + >>> period.until(1, year) Period(unit=year, start=Instant(year=2023, month=1, day=1), size=1) - >>> period.until(year, 1) - Period(unit=year, start=Instant(year=2023, month=1, day=1), size=1) + >>> period.until(2, years) + Period(unit=year, start=Instant(year=2023, month=1, day=1), size=2) .. versionadded:: 39.0.0 @@ -433,12 +434,12 @@ def until(self, unit: DateUnit, size: int = 1) -> Period: return Period(unit, start, size) - def last(self, unit: DateUnit, size: int = 1) -> Period: + def last(self, size: int, unit: DateUnit) -> Period: """Last ``size`` ``unit``s from ``Period.start``. Args: - unit: The unit of the requested Period. size: The number of units to include in the Period. + unit: The unit of the requested Period. Returns: A Period. @@ -450,29 +451,29 @@ def last(self, unit: DateUnit, size: int = 1) -> Period: >>> period = Period(year, start, 3) - >>> period.last(day) + >>> period.last(1, day) Period(unit=day, start=Instant(year=2022, month=12, day=31), size=1) - >>> period.last(day, 7) + >>> period.last(7, days) Period(unit=day, start=Instant(year=2022, month=12, day=25), size=7) - >>> period.last(month) + >>> period.last(1, month) Period(unit=month, start=Instant(year=2022, month=12, day=1), size=1) - >>> period.last(month, 3) + >>> period.last(3, months) Period(unit=month, start=Instant(year=2022, month=10, day=1), size=3) - >>> period.last(year) + >>> period.last(1, year) Period(unit=year, start=Instant(year=2022, month=1, day=1), size=1) - >>> period.last(year, 1) - Period(unit=year, start=Instant(year=2022, month=1, day=1), size=1) + >>> period.last(2, years) + Period(unit=year, start=Instant(year=2021, month=1, day=1), size=2) .. versionadded:: 39.0.0 """ - start: Instant = self.ago(unit, size).start + start: Instant = self.ago(size, unit).start return Period(unit, start, size) diff --git a/tests/core/test_countries.py b/tests/core/test_countries.py index 74fd34f38a..a12202068a 100644 --- a/tests/core/test_countries.py +++ b/tests/core/test_countries.py @@ -71,7 +71,7 @@ def test_divide_option_on_month_defined_variable(simulation): @pytest.mark.parametrize("simulation", [({}, PERIOD)], indirect = True) def test_divide_option_with_complex_period(simulation): - quarter = PERIOD.last(periods.MONTH, 3) + quarter = PERIOD.last(3, periods.months) with pytest.raises(ValueError) as error: simulation.household("housing_tax", quarter, options = [populations.DIVIDE]) diff --git a/tests/core/test_cycles.py b/tests/core/test_cycles.py index d1a4e6c358..e36e8bb647 100644 --- a/tests/core/test_cycles.py +++ b/tests/core/test_cycles.py @@ -44,7 +44,7 @@ class variable3(Variable): definition_period = periods.MONTH def formula(person, period): - return person('variable4', period.last(periods.MONTH)) + return person('variable4', period.last(1, periods.month)) class variable4(Variable): @@ -64,7 +64,7 @@ class variable5(Variable): definition_period = periods.MONTH def formula(person, period): - variable6 = person('variable6', period.last(periods.MONTH)) + variable6 = person('variable6', period.last(1, periods.month)) return 5 + variable6 @@ -96,7 +96,7 @@ class cotisation(Variable): def formula(person, period): if period.start.month == 12: - return 2 * person('cotisation', period.last(periods.MONTH)) + return 2 * person('cotisation', period.last(1, periods.month)) else: return person.empty_array() + 1 @@ -128,7 +128,7 @@ def test_spirals_result_in_default_value(simulation, reference_period): def test_spiral_heuristic(simulation, reference_period): variable5 = simulation.calculate('variable5', period = reference_period) variable6 = simulation.calculate('variable6', period = reference_period) - variable6_last_month = simulation.calculate('variable6', reference_period.last(periods.MONTH)) + variable6_last_month = simulation.calculate('variable6', reference_period.last(1, periods.month)) tools.assert_near(variable5, [11]) tools.assert_near(variable6, [11]) tools.assert_near(variable6_last_month, [11]) @@ -141,6 +141,6 @@ def test_spiral_cache(simulation, reference_period): def test_cotisation_1_level(simulation, reference_period): - month = reference_period.last(periods.MONTH) + month = reference_period.last(1, periods.month) cotisation = simulation.calculate('cotisation', period = month) tools.assert_near(cotisation, [0])