From 507a8e26fe315dbcbb308d2a86bfaaf7ca4fafee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 20 Sep 2019 11:36:05 +0200 Subject: [PATCH] Add start value to PYSON Date and DateTime This enable the customization of the starting point for delta operations. issue8561 review264041002 --- CHANGELOG | 1 + doc/ref/pyson.rst | 46 ++++++++++++++------------- trytond/pyson.py | 31 ++++++++++++++----- trytond/tests/test_pyson.py | 62 +++++++++++++++++++++++++++++++++++-- 4 files changed, 108 insertions(+), 32 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6d5c16848c..3e983e0c3e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +* Add start value to PYSON Date and DateTime * Add MultiSelection field * Support dot notation on PYSON Eval * Add __slots__ diff --git a/doc/ref/pyson.rst b/doc/ref/pyson.rst index d6f603a1fa..5207e5bae5 100644 --- a/doc/ref/pyson.rst +++ b/doc/ref/pyson.rst @@ -176,17 +176,16 @@ statement for look-up dictionary or integer objects. Returns true when a list (or dictionary) object named by ``obj`` contains the value of the variable (or key) named by ``key``. Otherwise returns false. -.. class:: Date([year[, month[, day[, delta_years[, delta_month[, delta_days]]]]]]) +.. class:: Date([year[, month[, day[, delta_years[, delta_month[, delta_days[, start]]]]]]]) -A :class:`Date` object represents the PYSON ``Date()`` -statement for date related conversions and basic calculations. -Returns a date object which represents -the values of arguments named by the *variables* explained below. -Missing values of arguments named by ``year`` or ``month`` or -``day`` take their defaults from the actual date. When values of -arguments named by ``delta_*`` are given, they are added to the -values of the appropriate arguments in a date and time preserving -manner. +A :class:`Date` object represents the PYSON ``Date()`` statement for date +related conversions and basic calculations. +Returns a date object which represents the values of arguments named by the +*variables* explained below. +Missing values of arguments named by ``year`` or ``month`` or ``day`` take +their defaults from ``start`` or the actual date. When values of arguments +named by ``delta_*`` are given, they are added to the values of the appropriate +arguments in a date and time preserving manner. Arguments: @@ -208,18 +207,20 @@ Arguments: ``delta_days`` Contains a PYSON statement of type int or long. -.. class:: DateTime([year[, month[, day[, hour[, minute[, second[, microsecond[, delta_years[, delta_months[, delta_days[, delta_hours[, delta_minutes[, delta_seconds[, delta_microseconds]]]]]]]]]]]]]]) +``start`` + Contains a PYSON statement of type date. -A :class:`DateTime` object represents the PYSON ``Date()`` -statement for date and time related conversions and calculations. -Returns a date time object which represents the values of -variables named by the *arguments* explained below. -Missing values of arguments named by ``year``, ``month``, ``day``, -``hour``, ``minute``, ``second``, ``microseconds`` take their -defaults from the actual date and time. -When values of arguments named by ``delta_*`` are given, these are -added to the appropriate attributes in a date and time preserving -manner. +.. class:: DateTime([year[, month[, day[, hour[, minute[, second[, microsecond[, delta_years[, delta_months[, delta_days[, delta_hours[, delta_minutes[, delta_seconds[, delta_microseconds[, start]]]]]]]]]]]]]]]) + +A :class:`DateTime` object represents the PYSON ``Date()`` statement for date +and time related conversions and calculations. +Returns a date time object which represents the values of variables named by +the *arguments* explained below. +Missing values of arguments named by ``year``, ``month``, ``day``, ``hour``, +``minute``, ``second``, ``microseconds`` take their defaults from ``start`` or +the actual date and time. +When values of arguments named by ``delta_*`` are given, these are added to +the appropriate attributes in a date and time preserving manner. Arguments: @@ -265,6 +266,9 @@ Arguments: ``delta_microseconds`` Contains a PYSON statement of type int or long. +``start`` + Contains a PYSON statement of type datetime. + .. class:: Len(value) A :class:`Len` object represents the PYSON ``Len()`` statement for length of a diff --git a/trytond/pyson.py b/trytond/pyson.py index c90b6ce78c..0d2c54f2f2 100644 --- a/trytond/pyson.py +++ b/trytond/pyson.py @@ -501,7 +501,7 @@ class In(PYSON): class Date(PYSON): def __init__(self, year=None, month=None, day=None, - delta_years=0, delta_months=0, delta_days=0, **kwargs): + delta_years=0, delta_months=0, delta_days=0, start=None, **kwargs): year = kwargs.get('y', year) month = kwargs.get('M', month) day = kwargs.get('d', day) @@ -522,11 +522,13 @@ class Date(PYSON): self._delta_years = delta_years self._delta_months = delta_months self._delta_days = delta_days + self._start = start @property def __repr_params__(self): return (self._year, self._month, self._day, - self._delta_years, self._delta_months, self._delta_days) + self._delta_years, self._delta_months, self._delta_days, + self._start) def pyson(self): return { @@ -537,6 +539,7 @@ class Date(PYSON): 'dy': self._delta_years, 'dM': self._delta_months, 'dd': self._delta_days, + 'start': self._start, } def types(self): @@ -544,7 +547,12 @@ class Date(PYSON): @staticmethod def eval(dct, context): - return datetime.date.today() + relativedelta( + today = dct.get('start') + if isinstance(today, datetime.datetime): + today = today.date() + if not isinstance(today, datetime.date): + today = datetime.date.today() + return today + relativedelta( year=dct['y'], month=dct['M'], day=dct['d'], @@ -560,7 +568,7 @@ class DateTime(Date): hour=None, minute=None, second=None, microsecond=None, delta_years=0, delta_months=0, delta_days=0, delta_hours=0, delta_minutes=0, delta_seconds=0, - delta_microseconds=0, **kwargs): + delta_microseconds=0, start=None, **kwargs): hour = kwargs.get('h', hour) minute = kwargs.get('m', minute) second = kwargs.get('s', second) @@ -571,7 +579,7 @@ class DateTime(Date): delta_microseconds = kwargs.get('dms', delta_microseconds) super(DateTime, self).__init__(year=year, month=month, day=day, delta_years=delta_years, delta_months=delta_months, - delta_days=delta_days, **kwargs) + delta_days=delta_days, start=start, **kwargs) for i in (hour, minute, second, microsecond, delta_hours, delta_minutes, delta_seconds, delta_microseconds): if isinstance(i, PYSON): @@ -594,9 +602,10 @@ class DateTime(Date): date_params = super(DateTime, self).__repr_params__ return (date_params[:3] + (self._hour, self._minute, self._second, self._microsecond) - + date_params[3:] + + date_params[3:-1] + (self._delta_hours, self._delta_minutes, self._delta_seconds, - self._delta_microseconds)) + self._delta_microseconds) + + date_params[-1:]) def pyson(self): res = super(DateTime, self).pyson() @@ -616,7 +625,13 @@ class DateTime(Date): @staticmethod def eval(dct, context): - return datetime.datetime.now() + relativedelta( + now = dct.get('start') + if (isinstance(now, datetime.date) + and not isinstance(now, datetime.datetime)): + now = datetime.datetime.combine(now, datetime.time()) + if not isinstance(now, datetime.datetime): + now = datetime.datetime.now() + return now + relativedelta( year=dct['y'], month=dct['M'], day=dct['d'], diff --git a/trytond/tests/test_pyson.py b/trytond/tests/test_pyson.py index 5294cc6433..674a1f0e10 100644 --- a/trytond/tests/test_pyson.py +++ b/trytond/tests/test_pyson.py @@ -408,7 +408,8 @@ class PYSONTestCase(unittest.TestCase): 'd': 12, 'dy': -1, 'dM': 12, - 'dd': -7 + 'dd': -7, + 'start': None, }) if not sys.flags.optimize: @@ -453,7 +454,33 @@ class PYSONTestCase(unittest.TestCase): datetime.date(2010, 2, 22)) self.assertEqual(repr(pyson.Date(2010, 1, 12, -1, 12, -7)), - 'Date(2010, 1, 12, -1, 12, -7)') + 'Date(2010, 1, 12, -1, 12, -7, None)') + + def test_Date_start(self): + "Test Date with start" + eval = pyson.PYSONEncoder().encode(pyson.Date( + start=pyson.Eval('date'))) + + date = datetime.date(2000, 1, 1) + self.assertEqual(pyson.PYSONDecoder( + {'date': date}).decode(eval), date) + + def test_Date_start_datetime(self): + "Test Date with start as datetime" + eval = pyson.PYSONEncoder().encode(pyson.Date( + start=pyson.Eval('datetime'))) + + datetime_ = datetime.datetime(2000, 1, 1, 12, 00) + self.assertEqual(pyson.PYSONDecoder( + {'datetime': datetime_}).decode(eval), datetime_.date()) + + def test_Date_start_invalid(self): + "Test Date with invalid start" + eval = pyson.PYSONEncoder().encode(pyson.Date( + start=pyson.Eval('foo'))) + + self.assertEqual(pyson.PYSONDecoder( + {'foo': 'bar'}).decode(eval), datetime.date.today()) def test_DateTime(self): 'Test pyson.DateTime' @@ -474,6 +501,7 @@ class PYSONTestCase(unittest.TestCase): 'dm': 15, 'ds': 30, 'dms': 1, + 'start': None, }) if not sys.flags.optimize: @@ -570,7 +598,35 @@ class PYSONTestCase(unittest.TestCase): self.assertEqual(repr(pyson.DateTime(2010, 1, 12, 10, 30, 20, 0, -1, 12, -7, 2, 15, 30, 1)), - 'DateTime(2010, 1, 12, 10, 30, 20, 0, -1, 12, -7, 2, 15, 30, 1)') + ('DateTime(2010, 1, 12, 10, 30, 20, 0, ' + '-1, 12, -7, 2, 15, 30, 1, None)')) + + def test_DateTime_start(self): + "Test DateTime with start" + eval = pyson.PYSONEncoder().encode(pyson.DateTime( + start=pyson.Eval('datetime'))) + + datetime_ = datetime.datetime(2000, 1, 1, 12, 0) + self.assertEqual(pyson.PYSONDecoder( + {'datetime': datetime_}).decode(eval), datetime_) + + def test_DateTime_start_datetime(self): + "Test DateTime with start as date" + eval = pyson.PYSONEncoder().encode(pyson.DateTime( + start=pyson.Eval('date'))) + + date = datetime.date(2000, 1, 1) + self.assertEqual(pyson.PYSONDecoder( + {'date': date}).decode(eval), + datetime.datetime(2000, 1, 1, 0, 0)) + + def test_DateTime_start_invalid(self): + "Test DateTime with invalid start" + eval = pyson.PYSONEncoder().encode(pyson.DateTime( + start=pyson.Eval('foo'))) + + self.assertIsInstance(pyson.PYSONDecoder( + {'foo': 'bar'}).decode(eval), datetime.datetime) def test_Len(self): 'Test pyson.Len'