diff options
author | Paul Ganssle <pganssle@users.noreply.github.com> | 2019-02-04 09:51:31 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-02-04 09:51:31 -0500 |
commit | 9b9d84dde19b8f0b4b568977079eead975941c87 (patch) | |
tree | e3c3ddd677bad304ed2e72163277b9945d32f2f7 | |
parent | b9aa7f0bf92961bfd91e36bbaff2d4c8c5abaf51 (diff) | |
parent | 399725924c561d3729c849d9297f92e5c5543df4 (diff) | |
download | dateutil-9b9d84dde19b8f0b4b568977079eead975941c87.tar.gz |
Merge pull request #859 from nicoe/exdate-params
Add parameters parsing to EXDATE
-rw-r--r-- | AUTHORS.md | 1 | ||||
-rw-r--r-- | changelog.d/410.feature.rst | 2 | ||||
-rw-r--r-- | dateutil/rrule.py | 119 | ||||
-rw-r--r-- | dateutil/test/test_rrule.py | 73 |
4 files changed, 143 insertions, 52 deletions
@@ -71,6 +71,7 @@ switch, and thus all their contributions are dual-licensed. - Michael J. Schultz <mjschultz@MASKED> - Mike Gilbert <floppym@MASKED> - Nicholas Herrriot <Nicholas.Herriot@gmail.com> **D** +- Nicolas Évrard (gh: @nicoe) **D** - Nick Smith <nick.smith@MASKED> - Orson Adams <orson.network@MASKED> (gh: @parsethis) **D** - Paul Dickson (gh @prdickson) **D** diff --git a/changelog.d/410.feature.rst b/changelog.d/410.feature.rst new file mode 100644 index 0000000..5e5e0d1 --- /dev/null +++ b/changelog.d/410.feature.rst @@ -0,0 +1,2 @@ +Added support for EXDATE parameters when parsing rrule strings. +Reported by @mlorant (gh issue #410), fixed by @nicoe (gh pr #859). diff --git a/dateutil/rrule.py b/dateutil/rrule.py index ea450f2..20a0c4a 100644 --- a/dateutil/rrule.py +++ b/dateutil/rrule.py @@ -435,7 +435,7 @@ class rrule(rrulebase): if not dtstart: if until and until.tzinfo: dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) - else: + else: dtstart = datetime.datetime.now().replace(microsecond=0) elif not isinstance(dtstart, datetime.datetime): dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) @@ -1412,6 +1412,8 @@ class rruleset(rrulebase): self._len = total + + class _rrulestr(object): """ Parses a string representation of a recurrence rule or set of recurrence rules. @@ -1557,6 +1559,58 @@ class _rrulestr(object): raise ValueError("invalid '%s': %s" % (name, value)) return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + def _parse_date_value(self, date_value, parms, rule_tzids, + ignoretz, tzids, tzinfos): + global parser + if not parser: + from dateutil import parser + + datevals = [] + value_found = False + TZID = None + + for parm in parms: + if parm.startswith("TZID="): + try: + tzkey = rule_tzids[parm.split('TZID=')[-1]] + except KeyError: + continue + if tzids is None: + from . import tz + tzlookup = tz.gettz + elif callable(tzids): + tzlookup = tzids + else: + tzlookup = getattr(tzids, 'get', None) + if tzlookup is None: + msg = ('tzids must be a callable, mapping, or None, ' + 'not %s' % tzids) + raise ValueError(msg) + + TZID = tzlookup(tzkey) + continue + + # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found + # only once. + if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: + raise ValueError("unsupported parm: " + parm) + else: + if value_found: + msg = ("Duplicate value parameter found in: " + parm) + raise ValueError(msg) + value_found = True + + for datestr in date_value.split(','): + date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) + if TZID is not None: + if date.tzinfo is None: + date = date.replace(tzinfo=TZID) + else: + raise ValueError('DTSTART/EXDATE specifies multiple timezone') + datevals.append(date) + + return datevals + def _parse_rfc(self, s, dtstart=None, cache=False, @@ -1629,54 +1683,18 @@ class _rrulestr(object): raise ValueError("unsupported EXRULE parm: "+parm) exrulevals.append(value) elif name == "EXDATE": - for parm in parms: - if parm != "VALUE=DATE-TIME": - raise ValueError("unsupported EXDATE parm: "+parm) - exdatevals.append(value) + exdatevals.extend( + self._parse_date_value(value, parms, + TZID_NAMES, ignoretz, + tzids, tzinfos) + ) elif name == "DTSTART": - # RFC 5445 3.8.2.4: The VALUE parameter is optional, but - # may be found only once. - value_found = False - TZID = None - valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"} - for parm in parms: - if parm.startswith("TZID="): - try: - tzkey = TZID_NAMES[parm.split('TZID=')[-1]] - except KeyError: - continue - if tzids is None: - from . import tz - tzlookup = tz.gettz - elif callable(tzids): - tzlookup = tzids - else: - tzlookup = getattr(tzids, 'get', None) - if tzlookup is None: - msg = ('tzids must be a callable, ' + - 'mapping, or None, ' + - 'not %s' % tzids) - raise ValueError(msg) - - TZID = tzlookup(tzkey) - continue - if parm not in valid_values: - raise ValueError("unsupported DTSTART parm: "+parm) - else: - if value_found: - msg = ("Duplicate value parameter found in " + - "DTSTART: " + parm) - raise ValueError(msg) - value_found = True - if not parser: - from dateutil import parser - dtstart = parser.parse(value, ignoretz=ignoretz, - tzinfos=tzinfos) - if TZID is not None: - if dtstart.tzinfo is None: - dtstart = dtstart.replace(tzinfo=TZID) - else: - raise ValueError('DTSTART specifies multiple timezones') + dtvals = self._parse_date_value(value, parms, TZID_NAMES, + ignoretz, tzids, tzinfos) + if len(dtvals) != 1: + raise ValueError("Multiple DTSTART values specified:" + + value) + dtstart = dtvals[0] else: raise ValueError("unsupported property: "+name) if (forceset or len(rrulevals) > 1 or rdatevals @@ -1698,10 +1716,7 @@ class _rrulestr(object): ignoretz=ignoretz, tzinfos=tzinfos)) for value in exdatevals: - for datestr in value.split(','): - rset.exdate(parser.parse(datestr, - ignoretz=ignoretz, - tzinfos=tzinfos)) + rset.exdate(value) if compatible and dtstart: rset.rdate(dtstart) return rset diff --git a/dateutil/test/test_rrule.py b/dateutil/test/test_rrule.py index cd08ce2..9dfa544 100644 --- a/dateutil/test/test_rrule.py +++ b/dateutil/test/test_rrule.py @@ -2853,6 +2853,74 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): datetime(1997, 9, 9, 9, 0), datetime(1997, 9, 16, 9, 0)]) + def testStrSetExDateMultiple(self): + rrstr = ("DTSTART:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" + "EXDATE:19970904T090000,19970911T090000,19970918T090000\n") + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 2, 9, 0), + datetime(1997, 9, 9, 9, 0), + datetime(1997, 9, 16, 9, 0)] + + def testStrSetExDateWithTZID(self): + BXL = tz.gettz('Europe/Brussels') + rr = rrulestr("DTSTART;TZID=Europe/Brussels:19970902T090000\n" + "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" + "EXDATE;TZID=Europe/Brussels:19970904T090000\n" + "EXDATE;TZID=Europe/Brussels:19970911T090000\n" + "EXDATE;TZID=Europe/Brussels:19970918T090000\n") + + assert list(rr) == [datetime(1997, 9, 2, 9, 0, tzinfo=BXL), + datetime(1997, 9, 9, 9, 0, tzinfo=BXL), + datetime(1997, 9, 16, 9, 0, tzinfo=BXL)] + + def testStrSetExDateValueDateTimeNoTZID(self): + rrstr = '\n'.join([ + "DTSTART:19970902T090000", + "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH", + "EXDATE;VALUE=DATE-TIME:19970902T090000", + "EXDATE;VALUE=DATE-TIME:19970909T090000", + ]) + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)] + + def testStrSetExDateValueMixDateTimeNoTZID(self): + rrstr = '\n'.join([ + "DTSTART:19970902T090000", + "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH", + "EXDATE;VALUE=DATE-TIME:19970902T090000", + "EXDATE:19970909T090000", + ]) + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 4, 9), datetime(1997, 9, 11, 9)] + + def testStrSetExDateValueDateTimeWithTZID(self): + BXL = tz.gettz('Europe/Brussels') + rrstr = '\n'.join([ + "DTSTART;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000", + "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH", + "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970902T090000", + "EXDATE;VALUE=DATE-TIME;TZID=Europe/Brussels:19970909T090000", + ]) + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 4, 9, tzinfo=BXL), + datetime(1997, 9, 11, 9, tzinfo=BXL)] + + def testStrSetExDateValueDate(self): + rrstr = '\n'.join([ + "DTSTART;VALUE=DATE:19970902", + "RRULE:FREQ=YEARLY;COUNT=4;BYDAY=TU,TH", + "EXDATE;VALUE=DATE:19970902", + "EXDATE;VALUE=DATE:19970909", + ]) + + rr = rrulestr(rrstr) + assert list(rr) == [datetime(1997, 9, 4), datetime(1997, 9, 11)] + def testStrSetDateAndExDate(self): self.assertEqual(list(rrulestr( "DTSTART:19970902T090000\n" @@ -2929,6 +2997,11 @@ class RRuleTest(WarningTestMixin, unittest.TestCase): self.assertEqual(list(rr), [datetime(1997, 9, 2, 0, 0, 0), datetime(1998, 9, 2, 0, 0, 0)]) + def testStrMultipleDTStartComma(self): + with pytest.raises(ValueError): + rr = rrulestr("DTSTART:19970101T000000,19970202T000000\n" + "RRULE:FREQ=YEARLY;COUNT=1") + def testStrInvalidUntil(self): with self.assertRaises(ValueError): list(rrulestr("DTSTART:19970902T090000\n" |