-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
FIX: properly set tz for YearLocator #12678
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
Add/modify a test? |
Yeah, good point - we should have caught this in testing before. |
245039b
to
1d5b5dc
Compare
1d5b5dc
to
b8207cb
Compare
test added... |
I have a bad feeling that this solution will fail on 3.5 for some datetime objects. As of 3.6 you can do datetime.astimezone, but before 3.6 you cannot if there is no timezone info. So I'm re-milestoning this fix for 3.1. There may be a way to work around this for 3.0.x, but I'm not clear what it is. |
OK, this will fail for py 3.5 if the user supplies a naive datetime (no timezone info) and they are using the |
|
dadeae6
to
34cd842
Compare
lib/matplotlib/dates.py
Outdated
try: | ||
ticks = [vmin.astimezone(self.tz)] | ||
except ValueError: | ||
raise ValueError('naive datetime objects cannot be used ' |
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.
This may be a good place to use the raise ... from e
functionality of python3 (https://www.python.org/dev/peps/pep-3134/)
try:
ticks = [vmin.astimezone(self.tz)]
except ValueError as E:
raise ValueError() from E
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.
OH, Cool, didn't know about that. I'll do soon.
lib/matplotlib/dates.py
Outdated
} | ||
# Note that tzinfo does not work with pytz timezones, and |
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.
This is moot if we drop py3.5?
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.
ah, this is targetted at 3.0.x so have to support 3.5 still.
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.
Yep... Could milestone 3.1 if you prefer.
attn @pganssle for advise on how to deal with timezones cleanly.... |
Requiring timezone aware datetimes to use the YearLocator seems like a pretty big API change? Am I misunderstanding this? |
No, this just fixes the YearLocator - it wasn't getting the timezone information passed to it properly, so 1 Jan 00:00 sometimes looked like 31 Dec 23:00, and the year was formatted as the previous year. This shouldn't affect the other Locators or Formatters in the autodateLocator (I'd hope we'd fail a few tests if it did!). |
lib/matplotlib/tests/test_dates.py
Outdated
for t_delta, expected in results: | ||
d2 = d1 + t_delta | ||
locator = _create_auto_date_locator(d1, d2) | ||
assert list(map(str, mdates.num2date(locator()))) == expected | ||
|
||
|
||
@pytest.mark.pytz | ||
@pytest.mark.skipif(not __has_pytz(), reason="Requires pytz") |
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.
Why are you using pytz
for this? Tests with pytz
are just to ensure matplotlib
is compatible with pytz
, but this is a general functionality test. It's better to use dateutil.tz
, which is not conditional on pytz
installation.
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.
The original bug report was pytz, and pytz doesn't work if the code is not as written in the PR. i.e. I needed to test pytz to get the PR to work, so it makes sense to test it here...
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.
Is it not possible to reproduce the bug with dateutil.tz
? Just because the reporter used pytz
doesn't mean you can't create an MWE without that dependency.
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.
Just checked, the original bug is also present when you use dateutil.tz
, with the original MWE now (more accurately in this case):
import matplotlib.pyplot as plt
from dateutil import tz
from datetime import datetime, timedelta
x = [datetime(2010, 1, 1, tzinfo=tz.gettz('America/New_York')) + timedelta(i)
for i in range(2000)]
plt.plot(x, [0] * len(x));
lib/matplotlib/dates.py
Outdated
} | ||
# Note that tzinfo does not work with pytz timezones, and |
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.
@pganssle : the way that tz info was put in before was to do vmin.replace(year=ymin, **self.replaced)
, but pytz
doesn't work properly (at all) with replace
. So we need a test with pytz to test that this code works.
To be honest, we roundtrip most dates (datetime-> ordinal -> datetime) so the fact that pytz doesn't do the right thing w/ tzinfo
isn't that big a deal practically, but it would be nice if it worked.
I guess we could add another test that doesn't use pytz, but I think pytz should be tested on this code path.
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.
replace(tzinfo=tz)
and astimezone(tz)
do two very different things.
If you think of time zones as units for datetimes, replace(tzinfo=tz)
is like getting a value that says "1 ft", scratching off the "ft" and adding "m" to get "1m". astimezone(tz)
is like getting something that says 1ft and converting it to meters to get 0.3m
.
Generally speaking, replace
is used if you know that the local portion of the datetime is correct and want to say what time zone it's in. astimezone
is used if you know that the absolute time represented by the datetime is correct and you want to convert it to a different local time.
In Python >=3.6, astimezone
can be used on naive datetimes, but what that does is that it interprets the naive datetime as local time, and converts that to the time zone of interest. What that means is that if you are in Tokyo and give this a naive datetime like datetime(2018, 1, 1)
and have YearLocator(tz=tz.gettz("America/New_York")
, that's going to get converted to 2017-12-31 10:00:00-05:00
, which is probably not what you wanted.
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.
pytz
doesn't work properly (at all) with replace. So we need a test with pytz to test that this code works.
Also, I'll note that I'm well aware of the limitations of pytz
.
I think probably it's best to use two tests, one with pytz
(skipped if pytz
is not present) and one with dateutil
. If you intend for the interface to look the same, you can likely parametrize the test function with a conditional skip on pytz
.
I haven't totally grokked how your code works, but it's worth noting that I solved a similar problem in the RRULE locator some time back, so I think you probably want to do something similar for |
34cd842
to
a6a96e3
Compare
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 would create a cbook.localize_tz()
helper to make this more readable. Something like (untested):
def localize_tz(date, tz):
if hasattr(tz, 'localize'):
return tz.localize(date, is_dst=True)
else:
return date.replace(tzinfo=tz)
Or alteratively, a cbook.datetime_replace()
that can handle the tz.
49b5d55
to
478ea3d
Compare
I didn't make a |
The solution with a private method is equally fine. |
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.
Looking at the code, you always do a replace followed by _localize_tz
. In that context a _date_replace
migh even be better. Less clutter in the calling code and for non-pytz even a bit faster since you don't replace twice.
def _date_replace(date, tzinfo=None, **kwargs):
"""
Drop-in replacement for datetime.replace() with support for pytz timezones.
"""
if hasattr(tzinfo, 'localize'):
date = date.replace(tzinfo=None, **kwargs)
return tzinfo.localize(date, is_dst=True)
return date.replace(tzinfo=tzinfo, **kwargs)
But I won't hold up if you want to stay with _localize_tz
.
lib/matplotlib/dates.py
Outdated
@@ -1409,13 +1419,22 @@ def tick_values(self, vmin, vmax): | |||
ymin = self.base.le(vmin.year) * self.base.step | |||
ymax = self.base.ge(vmax.year) * self.base.step | |||
|
|||
ticks = [vmin.replace(year=ymin, **self.replaced)] | |||
vmin = vmin.replace(year=ymin, **self.replaced) | |||
print('vmin before', vmin) |
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.
Debug print?
print('vmin before', vmin) |
lib/matplotlib/dates.py
Outdated
vmin = vmin.replace(year=ymin, **self.replaced) | ||
print('vmin before', vmin) | ||
vmin = _localize_tz(vmin, self.tz) | ||
print('vmin after', vmin) |
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.
Debug print?
print('vmin after', vmin) |
478ea3d
to
b5af9a9
Compare
Fair enough, that's an easy refactor.... |
@@ -1426,7 +1441,9 @@ def autoscale(self): | |||
ymin = self.base.le(dmin.year) | |||
ymax = self.base.ge(dmax.year) | |||
vmin = dmin.replace(year=ymin, **self.replaced) | |||
vmin = vmin.astimezone(self.tz) |
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.
Is this acutally different from he above cases?
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.
Ummm, hmmm, not sure this was properly tested....
0478bc1
to
b244b3d
Compare
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 we worked everything out on this one, yeah? I think this can be merged.
b244b3d
to
5ec912f
Compare
Rebased. Note two reviews... |
The other reviewer does not have write access. Do we count this as a full review? |
No not really. Just hoping that someone else would take a quick look on the strength of the existing reviews and merge. |
FIX: add check for py3.5 to fail if naive datetime TST: make pytz yearlylocator test
5ec912f
to
d8fcd82
Compare
Owee, I'm MrMeeseeks, Look at me. There seem to be a conflict, please backport manually. Here are approximate instructions:
And apply the correct labels and milestones. Congratulation you did some good work ! Hopefully your backport PR will be tested by the continuous integration and merged soon! If these instruction are inaccurate, feel free to suggest an improvement. |
PR Summary
dates.YearLocator
was not always being called with the timezone info. Now it is...Closes #12675:
Now has the first tick at 2010 instead of 2009....
PR Checklist