Repository: dateutil/dateutil (Python)

Issue:
Default DTSTART in rrule lacks a timezone
Since 2.7, the following code fails in our project:

```python
rule = rrule.rrulestr("RRULE:FREQ=WEEKLY;WKST=MO;BYDAY=MO,TU;UNTIL=20180331T200000Z")
```

This is the error we see:

```python-traceback
ValueError: RRULE UNTIL values must be specified in UTC when DTSTART is timezone-aware
```

The comment around the exception mentions this:

```python
# According to RFC5545 Section 3.3.10:
# https://tools.ietf.org/html/rfc5545#section-3.3.10
#
# > If the "DTSTART" property is specified as a date with UTC
# > time or a date with local time and time zone reference,
# > then the UNTIL rule part MUST be specified as a date with
# > UTC time.
```

We however do not specify the DTSTART property. Dateutil defaults to the current date in this case. However, said default does not have a timezone. Because we do however pass a timezone with our UNTIL property, we run into this exception.

Now I think you can make the argument that this is really our problem and it's probably best anyway to be specific with the DTSTART property (which is how we're going to work around this problem). However, I think it might also make sense to apply the timezone of the UNTIL property when the DTSTART date is created by dateutil.

This is related to #620 and #634.



Hints:
Hm. It is unclear to me whether the RFC requires DTSTART or not. I think it *is* required for some iCalendar components (e.g. VEVENT), but that the RRULE itself can take DTSTART from its parent element.

If we're allowing the generation of arbitrary RRULE elements without `dtstart` and filling in the current time as `dtstart`, then I think it's fair to generate a `dtstart` in the same time zone that `until` specifies.
I've been looking at this issue over the past few days. From what I've read in the RFC, I see nothing the prohibits adjusting the time zone for an automatically generated DTSTART to match that of UNTIL when DTSTART is not explicitly specified.

I believe I have come up with a working change to the implementation, but am running into an interesting challenge when writing a test case. Ideally, the test case should compare the behavior of a rule when DTSTART is explicitly specified to the results of another rrule where DTSTART is not specified. Writing a test case like that would involve DTSTARTs being generated using datetime.now([tz]) at two different times, in which case there may enough of a difference between the two DTSTARTs to cause the rrules to have different results. Using rrules with larger frequency would minimize the chance of the problem occurring, but not completely eliminate it. Maybe I am overthinking the problem, but I figured that I would bring it up before going any further.

There is of course the option of writing a test case that is less thorough but does not have the potential to fail in that way.
This is why we employ [freezegun](https://github.com/spulec/freezegun). [Example use here](https://github.com/dateutil/dateutil/blob/master/dateutil/test/test_utils.py#L18).

You can do something like this:
```python
from freezegun import freeze_time

@freeze_time(datetime(2018, 4, 7, 12, 30))
def test_dtstart_timezone:
    <test goes here>
```

Use pytest style tests instead of the unittest style tests that most of the test suite uses. We're trying to transition over to pytest.

Patch:
diff --git a/dateutil/rrule.py b/dateutil/rrule.py
--- a/dateutil/rrule.py
+++ b/dateutil/rrule.py
@@ -427,7 +427,10 @@ def __init__(self, freq, dtstart=None,
         super(rrule, self).__init__(cache)
         global easter
         if not dtstart:
-            dtstart = datetime.datetime.now().replace(microsecond=0)
+            if until and until.tzinfo:
+                dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
+            else:           
+                dtstart = datetime.datetime.now().replace(microsecond=0)
         elif not isinstance(dtstart, datetime.datetime):
             dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
         else:


Test patch:
diff --git a/dateutil/test/test_rrule.py b/dateutil/test/test_rrule.py
--- a/dateutil/test/test_rrule.py
+++ b/dateutil/test/test_rrule.py
@@ -14,7 +14,12 @@
     MO, TU, WE, TH, FR, SA, SU
 )
 
+from freezegun import freeze_time
 
+import pytest
+
+
+@pytest.mark.rrule
 class RRuleTest(WarningTestMixin, unittest.TestCase):
     def _rrulestr_reverse_test(self, rule):
         """
@@ -4538,6 +4543,31 @@ def testReplaceIfNotSet(self):
                              [datetime(1997, 1, 6)])
 
 
+@pytest.mark.rrule
+@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC))
+def test_generated_aware_dtstart():
+    dtstart_exp = datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC)
+    UNTIL = datetime(2018, 3, 6, 8, 0, tzinfo=tz.UTC)
+
+    rule_without_dtstart = rrule(freq=HOURLY, until=UNTIL)
+    rule_with_dtstart = rrule(freq=HOURLY, dtstart=dtstart_exp, until=UNTIL)
+    assert list(rule_without_dtstart) == list(rule_with_dtstart)
+
+
+@pytest.mark.rrule
+@pytest.mark.rrulestr
+@pytest.mark.xfail(reason="rrulestr loses time zone, gh issue #637")
+@freeze_time(datetime(2018, 3, 6, 5, 36, tzinfo=tz.UTC))
+def test_generated_aware_dtstart_rrulestr():
+    rrule_without_dtstart = rrule(freq=HOURLY,
+                                  until=datetime(2018, 3, 6, 8, 0,
+                                                 tzinfo=tz.UTC))
+    rrule_r = rrulestr(str(rrule_without_dtstart))
+
+    assert list(rrule_r) == list(rrule_without_dtstart)
+
+
+@pytest.mark.rruleset
 class RRuleSetTest(unittest.TestCase):
     def testSet(self):
         rrset = rruleset()