-
Notifications
You must be signed in to change notification settings - Fork 523
add support for DTSTART;TZID= in rrules #619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@pganssle one oddity I've noticed - and this is why the tests are failing on Travis-CI but they pass for me locally - when you parse the string this way, This causes the actual Do you have any thoughts on the proper way to solve this? |
dateutil/rrule.py
Outdated
| for parm in parms: | ||
| if parm.startswith("TZID="): | ||
| tzkey = parm.split('TZID=')[-1] | ||
| tzlookup = tzinfos.get if tzinfos else tz.gettz |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's best to explicitly compare to None, since I would think that tzinfos={} should not be equivalent to tzinfos=tz.gettz.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I'm confused about when you would want to use tz.gettz. Wouldn't it be any time tzinfos wasn't a dictionary with explicit keys?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ryanpetrello I am fine with tz.gettz being the default if you don't specify anything, but tzinfos can also be a callable, like this:
def my_tzinfos(tzid):
if tzid in SOME_LOOKUP_TABLE:
return tzid[SOME_LOOKUP_TABLE]
else:
try:
return dateutil.tz.tzstr(tzid)
except ValueError:
return tz.tzutc()I don't really know if that particular function is something someone would actually want to do, but the point is that tzinfos is basically a mapping between tzid and a tzinfo object, so any function that provides tzinfo as a function of tzid should work. As a convenience, you can also provide a dictionary directly, rather than passing mytzinfos.get.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if callable(tzinfos):
tzlookup = tzinfos
elif isinstance(tzinfos, dict):
tzlookup = tzinfos.get
else:
tzlookup = tz.gettzMore like this?
|
@ryanpetrello Hmmm.. That might be a tough one to fix if most of the rest of the function is assuming the string is already upper-cased. |
|
@pganssle Here's one way we could solve this, but it's pretty gross: diff --git a/dateutil/rrule.py b/dateutil/rrule.py
index 2f2511c..00edaff 100644
--- a/dateutil/rrule.py
+++ b/dateutil/rrule.py
@@ -1502,6 +1502,7 @@ class _rrulestr(object):
if compatible:
forceset = True
unfold = True
+ orig_s = s
s = s.upper()
if not s.strip():
raise ValueError("empty string")
@@ -1569,6 +1570,13 @@ class _rrulestr(object):
for parm in parms:
if parm.startswith("TZID="):
tzkey = parm.split('TZID=')[-1]
+ # the string is uppercase, so we have to find the
+ # TZID=value in its original case
+ try:
+ tzpos = orig_s.upper().index(tzkey)
+ tzkey = orig_s[tzpos:tzpos+len(tzkey)]
+ except IndexError:
+ continue
tzlookup = tzinfos.get if tzinfos else tz.gettz
TZID = tzlookup(tzkey)
continueMaybe an alternative would be to parse a mapping of the potential diff --git a/dateutil/rrule.py b/dateutil/rrule.py
index 2f2511c..08b044c 100644
--- a/dateutil/rrule.py
+++ b/dateutil/rrule.py
@@ -8,6 +8,7 @@ including support for caching of results.
import itertools
import datetime
import calendar
+import re
import sys
try:
@@ -1502,6 +1503,8 @@ class _rrulestr(object):
if compatible:
forceset = True
unfold = True
+
+ TZID_NAMES = dict(map(lambda x: (x.upper(), x), re.findall('TZID=(?P<name>[^:]+):', s)))
s = s.upper()
if not s.strip():
raise ValueError("empty string")
@@ -1568,7 +1571,10 @@ class _rrulestr(object):
valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"}
for parm in parms:
if parm.startswith("TZID="):
- tzkey = parm.split('TZID=')[-1]
+ tzkey = TZID_NAMES[parm.split('TZID=')[-1]]
tzlookup = tzinfos.get if tzinfos else tz.gettz
TZID = tzlookup(tzkey)
continue |
|
@pganssle is this current patch looking any better to you? |
|
@ryanpetrello Sorry, just had a baby. It will be a bit before I can review this PR. |
|
👶👶👶 congrats! Don't let me bother you, then - get to this whenever you find time 😄 |
| tzlookup = tzinfos | ||
| elif isinstance(tzinfos, dict): | ||
| tzlookup = tzinfos.get | ||
| else: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once again I don't think this is the right thing to do. tzlookup should be set to tz.gettz if and only if the user requested the default behavior.
I would probably do it this way:
if tzinfos is None:
tzlookup = tz.gettz
elif callable(tzinfos):
tzlookup = tzinfos
else:
tzlookup = getattr(tzinfos, 'get', None)
if tzlookup is None:
raise ValueError('tzinfos must be a callable or mapping')| forceset = True | ||
| unfold = True | ||
|
|
||
| TZID_NAMES = dict(map( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both dict and map are very slow compared to a dictionary comprehension in this case:
TZID_NAMES = {x.upper(): x
for x in re.findall('TZID=(?P<name>[^:]+):', s)}That said, I don't love this approach (though it may be the best option, unfortunately). I'd fix the comprehension part but don't worry about the regex part - we can fix that with a bigger refactor later I think.
| if dtstart.tzinfo is None: | ||
| dtstart = dtstart.replace(tzinfo=TZID) | ||
| else: | ||
| raise ValueError('DTSTART specifies multiple timezones') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you happen to check of the RFC mentions whether or not this is allowed?
This is one of the two reasonable options - the other being that if DTSTART is timezone-aware (e.g. 20180204T0800Z) and the rule has a specific time zone, you convert DTSTART into that time zone and generate dates starting from there. If the RFC is not silent on this point, we could choose either one, really, this one being more conservative and so probably preferable for a first release.
| datetime(1998, 9, 2, 9, 0), | ||
| datetime(1999, 9, 2, 9, 0)]) | ||
|
|
||
| def testStrWithTZID(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should add some tests for actually specifying the tzinfos parameter, and especially the case of tzinfos={} (which should fail to parse any TZID specified).
|
@ryanpetrello I was going to push directly to your branch, but you made this PR from the master branch of your fork, which is a PITA because I have my own master branch on my local fork, and it means that I can't easily track your upstream branch. In the future it's best to create a branch on your fork before making a PR. My updated version of this TZID support is in #624, and is slated for inclusion in 2.7.0 (which should go out this weekend). |
see: #614