From 39bd57e76ab9c06e6d932c7197e1312e7dd371d5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 10 Mar 2021 21:32:05 +0100 Subject: [PATCH 01/15] Fix ignored period tests --- tests/core/test_periods.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 8fa2619c6b..6f4b71d561 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -178,7 +178,7 @@ def test_empty_string(): period('') -@pytest.mark.parametrize("test", [ +@pytest.mark.parametrize("period, unit, length, first, last", [ (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')), @@ -187,12 +187,8 @@ def test_empty_string(): (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) +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 From 98da1ecc8f04fd3568ca751ba9ba6f35a815522a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 10 Mar 2021 21:41:30 +0100 Subject: [PATCH 02/15] Use fixtures --- tests/core/test_periods.py | 69 ++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 6f4b71d561..a8d7489262 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -1,12 +1,19 @@ # -*- coding: utf-8 -*- -import pytest +from pytest import fixture, mark, raises from openfisca_core.periods import Period, Instant, YEAR, MONTH, DAY, period -first_jan = Instant((2014, 1, 1)) -first_march = Instant((2014, 3, 1)) + +@fixture +def first_jan(): + return Instant((2014, 1, 1)) + + +@fixture +def first_march(): + return Instant((2014, 3, 1)) ''' @@ -16,42 +23,44 @@ # Years -def test_year(): +def test_year(first_jan): assert str(Period((YEAR, first_jan, 1))) == '2014' -def test_12_months_is_a_year(): +def test_12_months_is_a_year(first_jan): assert str(Period((MONTH, first_jan, 12))) == '2014' -def test_rolling_year(): +def test_rolling_year(first_march): assert str(Period((MONTH, first_march, 12))) == 'year:2014-03' assert str(Period((YEAR, first_march, 1))) == 'year:2014-03' -def test_several_years(): +def test_several_years(first_jan, first_march): 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(): + +def test_month(first_jan): assert str(Period((MONTH, first_jan, 1))) == '2014-01' -def test_several_months(): +def test_several_months(first_jan, first_march): assert str(Period((MONTH, first_jan, 3))) == 'month:2014-01:3' assert str(Period((MONTH, first_march, 3))) == 'month:2014-03:3' # Days -def test_day(): + +def test_day(first_jan): assert str(Period((DAY, first_jan, 1))) == '2014-01-01' -def test_several_days(): +def test_several_days(first_jan, first_march): assert str(Period((DAY, first_jan, 3))) == 'day:2014-01-01:3' assert str(Period((DAY, first_march, 3))) == 'day:2014-03-01:3' @@ -63,50 +72,53 @@ def test_several_days(): # Years -def test_parsing_year(): + +def test_parsing_year(first_jan): assert period('2014') == Period((YEAR, first_jan, 1)) -def test_parsing_rolling_year(): +def test_parsing_rolling_year(first_march): assert period('year:2014-03') == Period((YEAR, first_march, 1)) -def test_parsing_several_years(): +def test_parsing_several_years(first_jan): assert period('year:2014:2') == Period((YEAR, first_jan, 2)) def test_wrong_syntax_several_years(): - with pytest.raises(ValueError): + with raises(ValueError): period('2014:2') # Months -def test_parsing_month(): + +def test_parsing_month(first_jan): assert period('2014-01') == Period((MONTH, first_jan, 1)) -def test_parsing_several_months(): +def test_parsing_several_months(first_march): assert period('month:2014-03:3') == Period((MONTH, first_march, 3)) def test_wrong_syntax_several_months(): - with pytest.raises(ValueError): + with raises(ValueError): period('2014-3:3') # Days -def test_parsing_day(): + +def test_parsing_day(first_jan): assert period('2014-01-01') == Period((DAY, first_jan, 1)) -def test_parsing_several_days(): +def test_parsing_several_days(first_march): assert period('day:2014-03-01:3') == Period((DAY, first_march, 3)) def test_wrong_syntax_several_days(): - with pytest.raises(ValueError): + with raises(ValueError): period('2014-2-3:2') @@ -145,40 +157,41 @@ def test_leap_year_size_in_days(): def test_2_years_size_in_days(): assert Period(('year', Instant((2014, 1, 1)), 2)).size_in_days == 730 + # Misc def test_ambiguous_period(): - with pytest.raises(ValueError): + with raises(ValueError): period('month:2014') def test_deprecated_signature(): - with pytest.raises(TypeError): + with raises(TypeError): period(MONTH, 2014) def test_wrong_argument(): - with pytest.raises(ValueError): + with raises(ValueError): period({}) def test_wrong_argument_1(): - with pytest.raises(ValueError): + with raises(ValueError): period([]) def test_none(): - with pytest.raises(ValueError): + with raises(ValueError): period(None) def test_empty_string(): - with pytest.raises(ValueError): + with raises(ValueError): period('') -@pytest.mark.parametrize("period, unit, length, first, last", [ +@mark.parametrize('period, unit, length, first, last', [ (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')), From a7b7495a22bb9d5afab6b20f19db3754f972a4fa Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 10 Mar 2021 22:51:52 +0100 Subject: [PATCH 03/15] Add first test for week --- openfisca_core/periods.py | 15 +++++++++++++++ tests/core/test_periods.py | 16 +++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/openfisca_core/periods.py b/openfisca_core/periods.py index d906794e94..e25ff396dd 100644 --- a/openfisca_core/periods.py +++ b/openfisca_core/periods.py @@ -19,6 +19,7 @@ DAY = 'day' +WEEK = 'week' MONTH = 'month' YEAR = 'year' ETERNITY = 'eternity' @@ -291,27 +292,34 @@ def __str__(self): >>> str(period(YEAR, '2014-2')) 'year:2014-02' + >>> str(period(MONTH, '2014-2')) '2014-02' >>> str(period(YEAR, 2012, size = 2)) 'year:2012:2' + >>> str(period(MONTH, 2012, size = 2)) 'month:2012-01:2' + >>> str(period(MONTH, 2012, size = 12)) '2012' >>> str(period(YEAR, '2012-3', size = 2)) 'year:2012-03:2' + >>> str(period(MONTH, '2012-3', size = 2)) 'month:2012-03:2' + >>> str(period(MONTH, '2012-3', size = 12)) 'year:2012-03' """ unit, start_instant, size = self + if unit == ETERNITY: return 'ETERNITY' + year, month, day = start_instant # 1 year long period @@ -322,13 +330,20 @@ def __str__(self): else: # rolling year return '{}:{}-{:02d}'.format(YEAR, year, month) + # simple month if unit == MONTH and size == 1: return '{}-{:02d}'.format(year, month) + # several civil years if unit == YEAR and month == 1: return '{}:{}:{}'.format(unit, year, size) + # 1 week + if (unit == WEEK and size == 1 and year and month and day): + year, week, _ = datetime.date(year, month, day).isocalendar() + return f'{year}-W{week}' + if unit == DAY: if size == 1: return '{}-{:02d}-{:02d}'.format(year, month, day) diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index a8d7489262..35de8420c8 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -3,7 +3,7 @@ from pytest import fixture, mark, raises -from openfisca_core.periods import Period, Instant, YEAR, MONTH, DAY, period +from openfisca_core.periods import Period, Instant, YEAR, MONTH, WEEK, DAY, period @fixture @@ -44,8 +44,9 @@ def test_several_years(first_jan, first_march): # Months -def test_month(first_jan): +def test_month(first_jan, first_march): assert str(Period((MONTH, first_jan, 1))) == '2014-01' + assert str(Period((MONTH, first_march, 1))) == '2014-03' def test_several_months(first_jan, first_march): @@ -53,11 +54,20 @@ def test_several_months(first_jan, first_march): assert str(Period((MONTH, first_march, 3))) == 'month:2014-03:3' +# Weeks + + +def test_week(first_jan, first_march): + assert str(Period((WEEK, first_jan, 1))) == '2014-W1' + assert str(Period((WEEK, first_march, 1))) == '2014-W9' + + # Days -def test_day(first_jan): +def test_day(first_jan, first_march): assert str(Period((DAY, first_jan, 1))) == '2014-01-01' + assert str(Period((DAY, first_march, 1))) == '2014-03-01' def test_several_days(first_jan, first_march): From e844b56a02d0524f3ff7736c23a1c0486c2fb2a8 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 10 Mar 2021 23:22:47 +0100 Subject: [PATCH 04/15] Add several weeks --- openfisca_core/periods.py | 20 ++++++++++++-------- tests/core/test_periods.py | 21 +++++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/openfisca_core/periods.py b/openfisca_core/periods.py index e25ff396dd..e4d7148081 100644 --- a/openfisca_core/periods.py +++ b/openfisca_core/periods.py @@ -18,11 +18,11 @@ from typing import Dict +ETERNITY = 'eternity' +YEAR = 'year' +MONTH = 'month' DAY = 'day' WEEK = 'week' -MONTH = 'month' -YEAR = 'year' -ETERNITY = 'eternity' INSTANT_PATTERN = re.compile(r'^\d{4}(?:-\d{1,2}){0,2}$') # matches '2015', '2015-01', '2015-01-01' @@ -321,6 +321,7 @@ def __str__(self): return 'ETERNITY' year, month, day = start_instant + _, week, _ = datetime.date(year, month, day).isocalendar() # 1 year long period if (unit == MONTH and size == 12 or unit == YEAR and size == 1): @@ -339,17 +340,20 @@ def __str__(self): if unit == YEAR and month == 1: return '{}:{}:{}'.format(unit, year, size) - # 1 week - if (unit == WEEK and size == 1 and year and month and day): - year, week, _ = datetime.date(year, month, day).isocalendar() - return f'{year}-W{week}' - if unit == DAY: if size == 1: return '{}-{:02d}-{:02d}'.format(year, month, day) else: return '{}:{}-{:02d}-{:02d}:{}'.format(unit, year, month, day, size) + # 1 week + if (unit == WEEK and size == 1 and year and month): + return f'{year}-W{week}' + + # several weeks + if (unit == WEEK and size > 1 and year and month): + return f'{unit}:{year}-W{week}:{size}' + # complex period return '{}:{}-{:02d}:{}'.format(unit, year, month, size) diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 35de8420c8..be01d83184 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -54,14 +54,6 @@ def test_several_months(first_jan, first_march): assert str(Period((MONTH, first_march, 3))) == 'month:2014-03:3' -# Weeks - - -def test_week(first_jan, first_march): - assert str(Period((WEEK, first_jan, 1))) == '2014-W1' - assert str(Period((WEEK, first_march, 1))) == '2014-W9' - - # Days @@ -75,6 +67,19 @@ def test_several_days(first_jan, first_march): assert str(Period((DAY, first_march, 3))) == 'day:2014-03-01:3' +# Weeks + + +def test_week(first_jan, first_march): + assert str(Period((WEEK, first_jan, 1))) == '2014-W1' + assert str(Period((WEEK, first_march, 1))) == '2014-W9' + + +def test_several_weeks(first_jan, first_march): + assert str(Period((WEEK, first_jan, 3))) == 'week:2014-W1:3' + assert str(Period((WEEK, first_march, 3))) == 'week:2014-W9:3' + + ''' Test String -> Period ''' From 7b0f13ff899ef158b90ffc94a232d8f35838c8f5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 10 Mar 2021 23:37:12 +0100 Subject: [PATCH 05/15] Add weekdays --- openfisca_core/periods.py | 15 ++++++++++++--- tests/core/test_periods.py | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/openfisca_core/periods.py b/openfisca_core/periods.py index e4d7148081..561993a45e 100644 --- a/openfisca_core/periods.py +++ b/openfisca_core/periods.py @@ -23,6 +23,7 @@ MONTH = 'month' DAY = 'day' WEEK = 'week' +WEEKDAY = 'weekday' INSTANT_PATTERN = re.compile(r'^\d{4}(?:-\d{1,2}){0,2}$') # matches '2015', '2015-01', '2015-01-01' @@ -321,7 +322,7 @@ def __str__(self): return 'ETERNITY' year, month, day = start_instant - _, week, _ = datetime.date(year, month, day).isocalendar() + _, week, weekday = datetime.date(year, month, day).isocalendar() # 1 year long period if (unit == MONTH and size == 12 or unit == YEAR and size == 1): @@ -347,13 +348,21 @@ def __str__(self): return '{}:{}-{:02d}-{:02d}:{}'.format(unit, year, month, day, size) # 1 week - if (unit == WEEK and size == 1 and year and month): + if (unit == WEEK and size == 1): return f'{year}-W{week}' # several weeks - if (unit == WEEK and size > 1 and year and month): + if (unit == WEEK and size > 1): return f'{unit}:{year}-W{week}:{size}' + # 1 weekday + if (unit == WEEKDAY and size == 1): + return f'{year}-W{week}-{weekday}' + + # several weekdays + if (unit == WEEKDAY and size > 1): + return f'{unit}:{year}-W{week}-{weekday}:{size}' + # complex period return '{}:{}-{:02d}:{}'.format(unit, year, month, size) diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index be01d83184..45c425e43a 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -3,7 +3,7 @@ from pytest import fixture, mark, raises -from openfisca_core.periods import Period, Instant, YEAR, MONTH, WEEK, DAY, period +from openfisca_core.periods import Period, Instant, YEAR, MONTH, DAY, WEEK, WEEKDAY, period @fixture @@ -80,6 +80,19 @@ def test_several_weeks(first_jan, first_march): assert str(Period((WEEK, first_march, 3))) == 'week:2014-W9:3' +# Weekdays + + +def test_weekday(first_jan, first_march): + assert str(Period((WEEKDAY, first_jan, 1))) == '2014-W1-3' + assert str(Period((WEEKDAY, first_march, 1))) == '2014-W9-6' + + +def test_several_weekdays(first_jan, first_march): + assert str(Period((WEEKDAY, first_jan, 3))) == 'weekday:2014-W1-3:3' + assert str(Period((WEEKDAY, first_march, 3))) == 'weekday:2014-W9-6:3' + + ''' Test String -> Period ''' From c3883f005a8cfef6cae6adc081eda456a0a4eafe Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 10 Mar 2021 23:39:07 +0100 Subject: [PATCH 06/15] Replace day with DAY --- tests/core/test_periods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 45c425e43a..db1914bcaa 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -151,11 +151,11 @@ def test_wrong_syntax_several_days(): def test_day_size_in_days(): - assert Period(('day', Instant((2014, 12, 31)), 1)).size_in_days == 1 + 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 + assert Period((DAY, Instant((2014, 12, 31)), 3)).size_in_days == 3 def test_month_size_in_days(): From a7d271ef46991b5cd1289613775a4ab89c91fc03 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Wed, 10 Mar 2021 23:40:12 +0100 Subject: [PATCH 07/15] Replace month/year with MONTH/YEAR --- tests/core/test_periods.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index db1914bcaa..f31700b16b 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -159,31 +159,31 @@ def test_3_day_size_in_days(): def test_month_size_in_days(): - assert Period(('month', Instant((2014, 12, 1)), 1)).size_in_days == 31 + 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 + 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 + 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 + 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 + 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 + 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 + assert Period((YEAR, Instant((2014, 1, 1)), 2)).size_in_days == 730 # Misc From 10cfec4261997307f12208c381c025f8fd863f7a Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 11 Mar 2021 00:56:34 +0100 Subject: [PATCH 08/15] Test parsing weeks --- openfisca_core/periods.py | 63 ++++++++++++++++++++++++++++++++++---- tests/core/test_periods.py | 26 ++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/openfisca_core/periods.py b/openfisca_core/periods.py index 561993a45e..a6379fd321 100644 --- a/openfisca_core/periods.py +++ b/openfisca_core/periods.py @@ -859,6 +859,40 @@ def parse_simple_period(value): else: return Period((YEAR, Instant((date.year, date.month, 1)), 1)) + def parse_week_period(value): + """ + Parses ISO week date periods, such as 2010-W3 or 2012-W5-7. + """ + + # If it's complex, next! + if len(value.split(':')) != 1: + return None + + # If it's just a year, next! + if len(value.split('-')) == 1: + return None + + # If there are no weeks, next! + if value.find("W") == -1: + return None + + value = value.replace("W", "") + components = list(map(int, value.split("-"))) + + # If it has no weekdays return a week period + if len(components) == 2: + year, week = components + date = datetime.date.fromisocalendar(year, week, 1) + return Period((WEEK, Instant((date.year, date.month, date.day)), 1)) + + # If it has weekdays return a weekday period + if len(components) == 3: + year, week, day = components + date = datetime.date.fromisocalendar(year, week, day) + return Period((WEEKDAY, Instant((date.year, date.month, date.day)), 1)) + + return None + def raise_error(value): message = linesep.join([ "Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '{}'.".format(value), @@ -873,11 +907,19 @@ def raise_error(value): # check the type if isinstance(value, int): return Period((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 + + # try to parse as a week period + period = parse_week_period(value) + if period is not None: return period @@ -889,23 +931,30 @@ def raise_error(value): # left-most component must be a valid unit unit = components[0] - if unit not in (DAY, MONTH, YEAR): + if unit not in (WEEKDAY, WEEK, DAY, MONTH, 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) + # or a valid week/day period + base_period = parse_week_period(components[1]) + + if not base_period: + raise_error(value) # period like year:2015-03 have a size of 1 if len(components) == 2: size = 1 + # if provided, make sure the size is an integer elif len(components) == 3: try: size = int(components[2]) except ValueError: raise_error(value) + # if there is more than 2 ":" in the string, the period is invalid else: raise_error(value) @@ -940,10 +989,12 @@ def key_period_size(period): def unit_weights(): return { - DAY: 100, - MONTH: 200, - YEAR: 300, - ETERNITY: 400, + WEEKDAY: 100, + WEEK: 200, + DAY: 300, + MONTH: 400, + YEAR: 500, + ETERNITY: 600, } diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index f31700b16b..6dea0e2742 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -16,6 +16,16 @@ def first_march(): return Instant((2014, 3, 1)) +@fixture +def first_week(): + return Instant((2013, 12, 30)) + + +@fixture +def ninetieth_week(): + return Instant((2014, 2, 24)) + + ''' Test Period -> String ''' @@ -186,6 +196,22 @@ def test_2_years_size_in_days(): assert Period((YEAR, Instant((2014, 1, 1)), 2)).size_in_days == 730 +# Weeks + + +def test_parsing_week(first_week): + assert period('2014-W1') == Period((WEEK, first_week, 1)) + + +def test_parsing_several_weeks(ninetieth_week): + assert period('week:2014-W9:3') == Period((WEEK, ninetieth_week, 3)) + + +def test_wrong_syntax_several_weeks(): + with raises(ValueError): + period('2014-W3:3') + + # Misc From 8f580d45d762b12b33900742e66c57848e198ae4 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 11 Mar 2021 01:13:31 +0100 Subject: [PATCH 09/15] Test parsing weekdays --- tests/core/test_periods.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/core/test_periods.py b/tests/core/test_periods.py index 6dea0e2742..7b76e16906 100644 --- a/tests/core/test_periods.py +++ b/tests/core/test_periods.py @@ -212,6 +212,22 @@ def test_wrong_syntax_several_weeks(): period('2014-W3:3') +# Weekdays + + +def test_parsing_weekday(first_jan): + assert period('2014-W1-3') == Period((WEEKDAY, first_jan, 1)) + + +def test_parsing_several_weekdays(first_march): + assert period('weekday:2014-W9-6:3') == Period((WEEKDAY, first_march, 3)) + + +def test_wrong_syntax_several_weekdays(): + with raises(ValueError): + period('2014-W3-7:3') + + # Misc From c557909d5d1c9695f0b01f53f192ce964500ebf5 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 11 Mar 2021 19:27:34 +0100 Subject: [PATCH 10/15] Add weeks to instant check --- openfisca_core/periods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openfisca_core/periods.py b/openfisca_core/periods.py index a6379fd321..91fa57017e 100644 --- a/openfisca_core/periods.py +++ b/openfisca_core/periods.py @@ -203,7 +203,7 @@ def offset(self, offset, unit): Instant((2014, 12, 31)) """ year, month, day = self - assert unit in (DAY, MONTH, YEAR), 'Invalid unit: {} of type {}'.format(unit, type(unit)) + assert unit in (WEEKDAY, WEEK, DAY, MONTH, YEAR), 'Invalid unit: {} of type {}'.format(unit, type(unit)) if offset == 'first-of': if unit == MONTH: day = 1 From 2b992ec07f1ab0f302fc902b1a81aa12d2bc0454 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Thu, 11 Mar 2021 19:53:52 +0100 Subject: [PATCH 11/15] Add weeks to holders --- openfisca_core/holders.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/openfisca_core/holders.py b/openfisca_core/holders.py index 233a368cbc..63d84f96e9 100644 --- a/openfisca_core/holders.py +++ b/openfisca_core/holders.py @@ -12,7 +12,7 @@ from openfisca_core.data_storage import InMemoryStorage, OnDiskStorage from openfisca_core.errors import PeriodMismatchError from openfisca_core.indexed_enums import Enum -from openfisca_core.periods import MONTH, YEAR, ETERNITY +from openfisca_core.periods import WEEK, MONTH, YEAR, ETERNITY from openfisca_core.tools import eval_expression log = logging.getLogger(__name__) @@ -157,6 +157,7 @@ def set_input(self, period, array): """ period = periods.period(period) + if period.unit == ETERNITY and self.variable.definition_period != ETERNITY: error_message = os.linesep.join([ 'Unable to set a value for variable {0} for ETERNITY.', @@ -171,16 +172,20 @@ def set_input(self, period, array): self.variable.definition_period, error_message ) + if self.variable.is_neutralized: warning_message = "You cannot set a value for the variable {}, as it has been neutralized. The value you provided ({}) will be ignored.".format(self.variable.name, array) return warnings.warn( warning_message, Warning ) + if self.variable.value_type in (float, int) and isinstance(array, str): array = eval_expression(array) + if self.variable.set_input: return self.variable.set_input(self, period, array) + return self._set(period, array) def _to_array(self, value): @@ -209,6 +214,7 @@ def _set(self, period, value): if self.variable.definition_period != ETERNITY: if period is None: raise ValueError('A period must be specified to set values, except for variables with 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' @@ -300,13 +306,19 @@ def set_input_divide_by_period(holder, period, array): """ if not isinstance(array, np.ndarray): array = np.array(array) + period_size = period.size period_unit = period.unit + if holder.variable.definition_period == WEEK: + cached_period_unit = periods.WEEK + if holder.variable.definition_period == MONTH: cached_period_unit = periods.MONTH + elif holder.variable.definition_period == YEAR: cached_period_unit = periods.YEAR + else: raise ValueError('set_input_divide_by_period can be used only for yearly or monthly variables.') From 21729ed88da145ccdf42efcb080531c77609921b Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 21 Mar 2021 20:09:42 +0100 Subject: [PATCH 12/15] Add basic unit test to variables --- tests/core/variables/test_setters.py | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 tests/core/variables/test_setters.py diff --git a/tests/core/variables/test_setters.py b/tests/core/variables/test_setters.py new file mode 100644 index 0000000000..597a8a9cb0 --- /dev/null +++ b/tests/core/variables/test_setters.py @@ -0,0 +1,49 @@ +from pytest import fixture + +from openfisca_core.entities import build_entity +from openfisca_core.variables import Variable + + +@fixture +def entity(): + """Create a martian entity for our variable setters tests.""" + return build_entity( + key = "martian", + plural = "martians", + label = "People from Mars", + is_person = True, + ) + + +@fixture +def attributes(entity): + """ + Example attributes of our variable. + + We need to set them to something as variable attributes are class + attributes, therefore we have to define them before we create the variable. + """ + return { + "value_type": bool, + "entity": entity, + "default_value": True, + "definition_period": "year", + "label": "My taxes", + "reference": "https://law.gov.example/my-taxes", + } + + +@fixture +def make_variable(attributes): + """ + Example variable. + + This fixture defines our variable dynamically. We do so because we want to + test the variable setters, called at the moment the variable is + instantiated. + """ + return type("my_taxes", (Variable,), attributes) + + +def test_variable(make_variable): + assert make_variable() From 74fd249aa4e1ad8f66060229eda23e1b46298880 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 21 Mar 2021 20:47:47 +0100 Subject: [PATCH 13/15] Add definition period test to variables --- openfisca_core/variables.py | 5 +- tests/core/variables/test___init__.py | 78 +++++++++++++++++++++++++++ tests/core/variables/test_setters.py | 49 ----------------- 3 files changed, 81 insertions(+), 51 deletions(-) create mode 100644 tests/core/variables/test___init__.py delete mode 100644 tests/core/variables/test_setters.py diff --git a/openfisca_core/variables.py b/openfisca_core/variables.py index c1ecd8ba8c..12c82864a3 100644 --- a/openfisca_core/variables.py +++ b/openfisca_core/variables.py @@ -13,7 +13,7 @@ from openfisca_core import periods from openfisca_core.entities import Entity from openfisca_core.indexed_enums import Enum, EnumArray, ENUM_ARRAY_DTYPE -from openfisca_core.periods import DAY, MONTH, YEAR, ETERNITY +from openfisca_core.periods import ETERNITY, YEAR, MONTH, DAY, WEEK, WEEKDAY from openfisca_core.tools import eval_expression @@ -61,6 +61,7 @@ }, } +ALLOWED_DEFINITION_PERIODS = (ETERNITY, YEAR, MONTH, DAY, WEEK, WEEKDAY) FORMULA_NAME_PREFIX = 'formula' @@ -167,7 +168,7 @@ def __init__(self, baseline_variable = None): else: self.default_value = self.set(attr, 'default_value', allowed_type = self.value_type, default = VALUE_TYPES[self.value_type].get('default')) self.entity = self.set(attr, 'entity', required = True, setter = self.set_entity) - self.definition_period = self.set(attr, 'definition_period', required = True, allowed_values = (DAY, MONTH, YEAR, ETERNITY)) + self.definition_period = self.set(attr, 'definition_period', required = True, allowed_values = ALLOWED_DEFINITION_PERIODS) self.label = self.set(attr, 'label', allowed_type = str, setter = self.set_label) self.end = self.set(attr, 'end', allowed_type = str, setter = self.set_end) self.reference = self.set(attr, 'reference', setter = self.set_reference) diff --git a/tests/core/variables/test___init__.py b/tests/core/variables/test___init__.py new file mode 100644 index 0000000000..2e5203fe55 --- /dev/null +++ b/tests/core/variables/test___init__.py @@ -0,0 +1,78 @@ +from pytest import fixture, mark, raises + +from openfisca_core.entities import build_entity +from openfisca_core.variables import Variable + + +@fixture +def entity(): + """Create a martian entity for our variable creation tests.""" + return build_entity( + key = "martian", + plural = "martians", + label = "People from Mars", + is_person = True, + ) + + +@fixture +def attributes(entity): + """ + Example attributes of our variable. + + We need to set them to something as variable attributes are class + attributes, therefore we have to define them before we create the variable. + """ + return { + "value_type": bool, + "entity": entity, + "default_value": True, + "definition_period": "year", + "label": "My taxes", + "reference": "https://law.gov.example/my-taxes", + } + + +@fixture +def make_variable(): + """ + Example variable. + + This fixture defines our variable dynamically. We do so because we want to + test the variable valid values, verified at the moment the variable is + instantiated. + """ + def _make_variable(attributes): + return type("my_taxes", (Variable,), attributes)() + + return _make_variable + + +def test_variable(make_variable, attributes): + assert make_variable(attributes) + + +# Definition periods + + +@mark.parametrize("definition_period", ["eternity", "year", "month", "day", "week", "weekday"]) +def test_variable_with_valid_definition_period(definition_period, make_variable, attributes): + attributes["definition_period"] = definition_period + assert make_variable(attributes) + + +@mark.parametrize("definition_period", ["never", 9000]) +def test_variable_with_invalid_definition_period(definition_period, make_variable, attributes): + with raises(ValueError) as error: + attributes["definition_period"] = definition_period + make_variable(attributes) + + assert f"Invalid value '{definition_period}' for attribute 'definition_period'" in str(error.value) + + +def test_variable_without_definition_period(make_variable, attributes): + with raises(ValueError) as error: + attributes["definition_period"] = None + make_variable(attributes) + + assert "Missing attribute 'definition_period'" in str(error.value) diff --git a/tests/core/variables/test_setters.py b/tests/core/variables/test_setters.py deleted file mode 100644 index 597a8a9cb0..0000000000 --- a/tests/core/variables/test_setters.py +++ /dev/null @@ -1,49 +0,0 @@ -from pytest import fixture - -from openfisca_core.entities import build_entity -from openfisca_core.variables import Variable - - -@fixture -def entity(): - """Create a martian entity for our variable setters tests.""" - return build_entity( - key = "martian", - plural = "martians", - label = "People from Mars", - is_person = True, - ) - - -@fixture -def attributes(entity): - """ - Example attributes of our variable. - - We need to set them to something as variable attributes are class - attributes, therefore we have to define them before we create the variable. - """ - return { - "value_type": bool, - "entity": entity, - "default_value": True, - "definition_period": "year", - "label": "My taxes", - "reference": "https://law.gov.example/my-taxes", - } - - -@fixture -def make_variable(attributes): - """ - Example variable. - - This fixture defines our variable dynamically. We do so because we want to - test the variable setters, called at the moment the variable is - instantiated. - """ - return type("my_taxes", (Variable,), attributes) - - -def test_variable(make_variable): - assert make_variable() From fcc978fb23bca00c4324e185c6cb3d1a8d7704c9 Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 21 Mar 2021 23:12:29 +0100 Subject: [PATCH 14/15] Add a first unit test to holders --- tests/core/test_holders.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/core/test_holders.py b/tests/core/test_holders.py index 9e62d6e64b..fe439f96ae 100644 --- a/tests/core/test_holders.py +++ b/tests/core/test_holders.py @@ -11,6 +11,9 @@ from openfisca_core.memory_config import MemoryConfig from openfisca_core.holders import Holder, set_input_dispatch_by_period from openfisca_core.errors import PeriodMismatchError +from openfisca_core.entities import build_entity +from openfisca_core.variables import Variable +from openfisca_core.populations import Population from .test_countries import tax_benefit_system from pytest import fixture @@ -212,3 +215,38 @@ def test_set_input_float_to_int(single): simulation.person.get_holder('age').set_input(period, age) result = simulation.calculate('age', period) assert result == np.asarray([50]) + + +# Unit tests - Set Input / Periods + + +ENTITY = build_entity( + key = "martian", + plural = "martians", + label = "People from Mars", + is_person = True, + ) + + +@fixture +def variable(): + class EternalVariable(Variable): + value_type = bool + entity = ENTITY + definition_period = ETERNITY + + return EternalVariable() + + +@fixture +def population(): + return Population(ENTITY) + + +@fixture +def holder(variable, population): + return Holder(variable, population) + + +def test_holder(holder): + assert holder From abe326a9dc9df212a03c4bee07a6e01ed7ec295d Mon Sep 17 00:00:00 2001 From: Mauko Quiroga Date: Sun, 21 Mar 2021 23:30:26 +0100 Subject: [PATCH 15/15] Do not support weeks on divide by period --- openfisca_core/holders.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/openfisca_core/holders.py b/openfisca_core/holders.py index 63d84f96e9..46dd900fe6 100644 --- a/openfisca_core/holders.py +++ b/openfisca_core/holders.py @@ -310,9 +310,6 @@ def set_input_divide_by_period(holder, period, array): period_size = period.size period_unit = period.unit - if holder.variable.definition_period == WEEK: - cached_period_unit = periods.WEEK - if holder.variable.definition_period == MONTH: cached_period_unit = periods.MONTH