-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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 EThere 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
| 'second': 0, | ||
| 'tzinfo': tz | ||
| } | ||
| # 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
|
|
||
|
|
||
| @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
| 'second': 0, | ||
| 'tzinfo': tz | ||
| } | ||
| # 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.
pytzdoesn'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. |
timhoffm
left a comment
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
|
|
||
| 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.... |
| 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
pganssle
left a comment
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.YearLocatorwas 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