Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d51b377

Browse files
author
Oscar Gustafsson
committed
All classes and methods in dates support both string and tzinfo as tz-argument
1 parent 4f72164 commit d51b377

File tree

2 files changed

+116
-70
lines changed

2 files changed

+116
-70
lines changed

lib/matplotlib/dates.py

Lines changed: 56 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,12 @@
8282
In [1]: date(2006, 4, 1).toordinal() - date(1, 1, 1).toordinal()
8383
Out[1]: 732401
8484
85-
All the Matplotlib date converters, tickers and formatters are timezone aware.
86-
If no explicit timezone is provided, :rc:`timezone` is assumed. If you want to
87-
use a custom time zone, pass a `datetime.tzinfo` instance with the tz keyword
88-
argument to `num2date`, `.Axis.axis_date`, and any custom date tickers or
89-
locators you create.
85+
All the Matplotlib date converters, tickers and formatters are time-zone aware.
86+
If no explicit time zone is provided, :rc:`timezone` is assumed, provided as a
87+
string. If you want to use a custom time zone, pass a `datetime.tzinfo`
88+
instance or a string with the time-zone name with the tz keyword argument to
89+
`num2date`, `.Axis.axis_date`, and any custom date tickers or locators you
90+
create. Time-zone strings are parsed by `~dateutil.tz.gettz`.
9091
9192
A wide range of specific and general purpose date tick locators and
9293
formatters are provided in this module. See
@@ -202,12 +203,24 @@
202203
UTC = datetime.timezone.utc
203204

204205

205-
def _get_rc_timezone():
206-
"""Retrieve the preferred timezone from the rcParams dictionary."""
207-
s = mpl.rcParams['timezone']
208-
if s == 'UTC':
209-
return UTC
210-
return dateutil.tz.gettz(s)
206+
def _get_tzinfo(tz=None):
207+
"""
208+
Generate tzinfo from a string or return tzinfo. If None (default),
209+
retrieve the preferred time zone from the rcParams dictionary.
210+
"""
211+
if tz is None:
212+
tz = mpl.rcParams['timezone']
213+
if tz == 'UTC':
214+
return UTC
215+
if isinstance(tz, str):
216+
tzinfo = dateutil.tz.gettz(tz)
217+
if tzinfo is None:
218+
raise ValueError(f"{tz} is not a valid time zone as parsed by"
219+
" dateutil.tz.gettz.")
220+
return tzinfo
221+
if isinstance(tz, datetime.tzinfo):
222+
return tz
223+
raise TypeError("tz must be string or tzinfo subclass.")
211224

212225

213226
"""
@@ -337,12 +350,11 @@ def _from_ordinalf(x, tz=None):
337350
338351
The input date *x* is a float in ordinal days at UTC, and the output will
339352
be the specified `.datetime` object corresponding to that time in
340-
timezone *tz*, or if *tz* is ``None``, in the timezone specified in
353+
time zone *tz*, or if *tz* is ``None``, in the time zone specified in
341354
:rc:`timezone`.
342355
"""
343356

344-
if tz is None:
345-
tz = _get_rc_timezone()
357+
tz = _get_tzinfo(tz)
346358

347359
dt = (np.datetime64(get_epoch()) +
348360
np.timedelta64(int(np.round(x * MUSECONDS_PER_DAY)), 'us'))
@@ -355,7 +367,7 @@ def _from_ordinalf(x, tz=None):
355367

356368
# datetime64 is always UTC:
357369
dt = dt.replace(tzinfo=dateutil.tz.gettz('UTC'))
358-
# but maybe we are working in a different timezone so move.
370+
# but maybe we are working in a different time zone so move.
359371
dt = dt.astimezone(tz)
360372
# fix round off errors
361373
if np.abs(x) > 70 * 365:
@@ -508,13 +520,13 @@ def num2date(x, tz=None):
508520
Number of days (fraction part represents hours, minutes, seconds)
509521
since the epoch. See `.get_epoch` for the
510522
epoch, which can be changed by :rc:`date.epoch` or `.set_epoch`.
511-
tz : str, default: :rc:`timezone`
512-
Timezone of *x*.
523+
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
524+
Time zone of *x*. If a string, *tz* is passed to `dateutil.tz`.
513525
514526
Returns
515527
-------
516528
`~datetime.datetime` or sequence of `~datetime.datetime`
517-
Dates are returned in timezone *tz*.
529+
Dates are returned in time zone *tz*.
518530
519531
If *x* is a sequence, a sequence of `~datetime.datetime` objects will
520532
be returned.
@@ -525,8 +537,7 @@ def num2date(x, tz=None):
525537
Gregorian calendar is assumed; this is not universal practice.
526538
For details, see the module docstring.
527539
"""
528-
if tz is None:
529-
tz = _get_rc_timezone()
540+
tz = _get_tzinfo(tz)
530541
return _from_ordinalf_np_vectorized(x, tz).tolist()
531542

532543

@@ -621,16 +632,14 @@ def __init__(self, fmt, tz=None, *, usetex=None):
621632
----------
622633
fmt : str
623634
`~datetime.datetime.strftime` format string
624-
tz : `datetime.tzinfo`, default: :rc:`timezone`
625-
Ticks timezone.
635+
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
636+
Ticks time zone.
626637
usetex : bool, default: :rc:`text.usetex`
627638
To enable/disable the use of TeX's math mode for rendering the
628639
results of the formatter.
629640
"""
630-
if tz is None:
631-
tz = _get_rc_timezone()
641+
self.tz = _get_tzinfo(tz)
632642
self.fmt = fmt
633-
self.tz = tz
634643
self._usetex = (usetex if usetex is not None else
635644
mpl.rcParams['text.usetex'])
636645

@@ -639,7 +648,7 @@ def __call__(self, x, pos=0):
639648
return _wrap_in_tex(result) if self._usetex else result
640649

641650
def set_tzinfo(self, tz):
642-
self.tz = tz
651+
self.tz = _get_tzinfo(tz)
643652

644653

645654
class ConciseDateFormatter(ticker.Formatter):
@@ -656,8 +665,8 @@ class ConciseDateFormatter(ticker.Formatter):
656665
locator : `.ticker.Locator`
657666
Locator that this axis is using.
658667
659-
tz : str, optional
660-
Passed to `.dates.date2num`.
668+
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
669+
Passed to `.dates.num2date`.
661670
662671
formats : list of 6 strings, optional
663672
Format strings for 6 levels of tick labelling: mostly years,
@@ -757,7 +766,7 @@ def __init__(self, locator, tz=None, formats=None, offset_formats=None,
757766

758767
if offset_formats:
759768
if len(offset_formats) != 6:
760-
raise ValueError('offsetfmts argument must be a list of '
769+
raise ValueError('offset_formats argument must be a list of '
761770
'6 format strings (or None)')
762771
self.offset_formats = offset_formats
763772
else:
@@ -927,8 +936,8 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *,
927936
locator : `.ticker.Locator`
928937
Locator that this axis is using.
929938
930-
tz : str, optional
931-
Passed to `.dates.date2num`.
939+
tz : str or `~datetime.tzinfo`, optional
940+
Passed to `.dates.num2date`.
932941
933942
defaultfmt : str
934943
The default format to use if none of the values in ``self.scaled``
@@ -1001,7 +1010,7 @@ def _update_rrule(self, **kwargs):
10011010
tzinfo = self._base_tzinfo
10021011

10031012
# rrule does not play nicely with time zones - especially pytz time
1004-
# zones, it's best to use naive zones and attach timezones once the
1013+
# zones, it's best to use naive zones and attach time zones once the
10051014
# datetimes are returned
10061015
if 'dtstart' in kwargs:
10071016
dtstart = kwargs['dtstart']
@@ -1103,17 +1112,15 @@ def __init__(self, tz=None):
11031112
"""
11041113
Parameters
11051114
----------
1106-
tz : `datetime.tzinfo`
1115+
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
11071116
"""
1108-
if tz is None:
1109-
tz = _get_rc_timezone()
1110-
self.tz = tz
1117+
self.tz = _get_tzinfo(tz)
11111118

11121119
def set_tzinfo(self, tz):
11131120
"""
1114-
Set time zone info.
1121+
Set time zone info. str or `~datetime.tzinfo`.
11151122
"""
1116-
self.tz = tz
1123+
self.tz = _get_tzinfo(tz)
11171124

11181125
def datalim_to_dt(self):
11191126
"""Convert axis data interval to datetime objects."""
@@ -1283,8 +1290,8 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
12831290
"""
12841291
Parameters
12851292
----------
1286-
tz : `datetime.tzinfo`
1287-
Ticks timezone.
1293+
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
1294+
Ticks time zone.
12881295
minticks : int
12891296
The minimum number of ticks desired; controls whether ticks occur
12901297
yearly, monthly, etc.
@@ -1303,7 +1310,7 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
13031310
the ticks to be at hours 0, 6, 12, 18 when hourly ticking is done
13041311
at 6 hour intervals.
13051312
"""
1306-
super().__init__(tz)
1313+
super().__init__(tz=tz)
13071314
self._freq = YEARLY
13081315
self._freqs = [YEARLY, MONTHLY, DAILY, HOURLY, MINUTELY,
13091316
SECONDLY, MICROSECONDLY]
@@ -1457,7 +1464,7 @@ def get_locator(self, dmin, dmax):
14571464
byhour=byhour, byminute=byminute,
14581465
bysecond=bysecond)
14591466

1460-
locator = RRuleLocator(rrule, self.tz)
1467+
locator = RRuleLocator(rrule, tz=self.tz)
14611468
else:
14621469
locator = MicrosecondLocator(interval, tz=self.tz)
14631470
if date2num(dmin) > 70 * 365 and interval < 1000:
@@ -1490,7 +1497,7 @@ def __init__(self, base=1, month=1, day=1, tz=None):
14901497
"""
14911498
rule = rrulewrapper(YEARLY, interval=base, bymonth=month,
14921499
bymonthday=day, **self.hms0d)
1493-
super().__init__(rule, tz)
1500+
super().__init__(rule, tz=tz)
14941501
self.base = ticker._Edge_integer(base, 0)
14951502

14961503
def _create_rrule(self, vmin, vmax):
@@ -1534,7 +1541,7 @@ def __init__(self, bymonth=None, bymonthday=1, interval=1, tz=None):
15341541

15351542
rule = rrulewrapper(MONTHLY, bymonth=bymonth, bymonthday=bymonthday,
15361543
interval=interval, **self.hms0d)
1537-
super().__init__(rule, tz)
1544+
super().__init__(rule, tz=tz)
15381545

15391546

15401547
class WeekdayLocator(RRuleLocator):
@@ -1562,7 +1569,7 @@ def __init__(self, byweekday=1, interval=1, tz=None):
15621569

15631570
rule = rrulewrapper(DAILY, byweekday=byweekday,
15641571
interval=interval, **self.hms0d)
1565-
super().__init__(rule, tz)
1572+
super().__init__(rule, tz=tz)
15661573

15671574

15681575
class DayLocator(RRuleLocator):
@@ -1588,7 +1595,7 @@ def __init__(self, bymonthday=None, interval=1, tz=None):
15881595

15891596
rule = rrulewrapper(DAILY, bymonthday=bymonthday,
15901597
interval=interval, **self.hms0d)
1591-
super().__init__(rule, tz)
1598+
super().__init__(rule, tz=tz)
15921599

15931600

15941601
class HourLocator(RRuleLocator):
@@ -1608,7 +1615,7 @@ def __init__(self, byhour=None, interval=1, tz=None):
16081615

16091616
rule = rrulewrapper(HOURLY, byhour=byhour, interval=interval,
16101617
byminute=0, bysecond=0)
1611-
super().__init__(rule, tz)
1618+
super().__init__(rule, tz=tz)
16121619

16131620

16141621
class MinuteLocator(RRuleLocator):
@@ -1628,7 +1635,7 @@ def __init__(self, byminute=None, interval=1, tz=None):
16281635

16291636
rule = rrulewrapper(MINUTELY, byminute=byminute, interval=interval,
16301637
bysecond=0)
1631-
super().__init__(rule, tz)
1638+
super().__init__(rule, tz=tz)
16321639

16331640

16341641
class SecondLocator(RRuleLocator):
@@ -1648,7 +1655,7 @@ def __init__(self, bysecond=None, interval=1, tz=None):
16481655
bysecond = range(60)
16491656

16501657
rule = rrulewrapper(SECONDLY, bysecond=bysecond, interval=interval)
1651-
super().__init__(rule, tz)
1658+
super().__init__(rule, tz=tz)
16521659

16531660

16541661
class MicrosecondLocator(DateLocator):

lib/matplotlib/tests/test_dates.py

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,9 @@ def _create_auto_date_locator(date1, date2):
446446
locator = mdates.AutoDateLocator(interval_multiples=False)
447447
assert locator.maxticks == {0: 11, 1: 12, 3: 11, 4: 12, 5: 11, 6: 11, 7: 8}
448448

449+
locator = mdates.AutoDateLocator(maxticks={dateutil.rrule.MONTHLY: 5})
450+
assert locator.maxticks == {0: 11, 1: 5, 3: 11, 4: 12, 5: 11, 6: 11, 7: 8}
451+
449452
locator = mdates.AutoDateLocator(maxticks=5)
450453
assert locator.maxticks == {0: 5, 1: 5, 3: 5, 4: 5, 5: 5, 6: 5, 7: 5}
451454

@@ -1234,14 +1237,50 @@ def test_julian2num():
12341237
assert mdates.num2julian(2.0) == 2440589.5
12351238

12361239

1237-
def test_rcparams_timezone():
1238-
assert mdates._get_rc_timezone() == mdates.UTC
1239-
tz = 'Europe/Iceland'
1240-
assert mdates._get_rc_timezone() != dateutil.tz.gettz(tz)
1240+
def test_DateLocator():
1241+
locator = mdates.DateLocator()
1242+
# Test nonsingular
1243+
assert locator.nonsingular(0, np.inf) == (10957.0, 14610.0)
1244+
assert locator.nonsingular(0, 1) == (0, 1)
1245+
assert locator.nonsingular(1, 0) == (0, 1)
1246+
assert locator.nonsingular(0, 0) == (-2, 2)
1247+
locator.create_dummy_axis()
1248+
# default values
1249+
assert locator.datalim_to_dt() == (
1250+
datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc),
1251+
datetime.datetime(1970, 1, 2, 0, 0, tzinfo=datetime.timezone.utc))
1252+
1253+
# Check default is UTC
1254+
assert locator.tz == mdates.UTC
1255+
tz_str = 'Iceland'
1256+
iceland_tz = dateutil.tz.gettz(tz_str)
1257+
# Check not Iceland
1258+
assert locator.tz != iceland_tz
1259+
# Set it to to Iceland
1260+
locator.set_tzinfo('Iceland')
1261+
# Check now it is Iceland
1262+
assert locator.tz == iceland_tz
1263+
locator.create_dummy_axis()
1264+
locator.axis.set_data_interval(*mdates.date2num(["2022-01-10",
1265+
"2022-01-08"]))
1266+
assert locator.datalim_to_dt() == (
1267+
datetime.datetime(2022, 1, 8, 0, 0, tzinfo=iceland_tz),
1268+
datetime.datetime(2022, 1, 10, 0, 0, tzinfo=iceland_tz))
1269+
1270+
# Set rcParam
1271+
plt.rcParams['timezone'] = tz_str
12411272

1242-
plt.rcParams['timezone'] = tz
1273+
# Create a new one in a similar way
1274+
locator = mdates.DateLocator()
1275+
# Check now it is Iceland
1276+
assert locator.tz == iceland_tz
12431277

1244-
assert mdates._get_rc_timezone() == dateutil.tz.gettz(tz)
1278+
# Test invalid tz values
1279+
with pytest.raises(ValueError, match="Aiceland is not a valid time zone"):
1280+
mdates.DateLocator(tz="Aiceland")
1281+
with pytest.raises(TypeError,
1282+
match="tz must be string or tzinfo subclass."):
1283+
mdates.DateLocator(tz=1)
12451284

12461285

12471286
def test_datestr2num():
@@ -1258,13 +1297,15 @@ def test_datestr2num():
12581297

12591298
def test_concise_formatter_exceptions():
12601299
locator = mdates.AutoDateLocator()
1261-
with pytest.raises(ValueError):
1300+
with pytest.raises(ValueError, match="formats argument must be a list"):
12621301
mdates.ConciseDateFormatter(locator, formats=['', '%Y'])
12631302

1264-
with pytest.raises(ValueError):
1303+
with pytest.raises(ValueError,
1304+
match="zero_formats argument must be a list"):
12651305
mdates.ConciseDateFormatter(locator, zero_formats=['', '%Y'])
12661306

1267-
with pytest.raises(ValueError):
1307+
with pytest.raises(ValueError,
1308+
match="offset_formats argument must be a list"):
12681309
mdates.ConciseDateFormatter(locator, offset_formats=['', '%Y'])
12691310

12701311

@@ -1275,15 +1316,13 @@ def test_concise_formatter_call():
12751316
assert formatter.format_data_short(19002.0) == '2022-01-10 00:00:00'
12761317

12771318

1278-
def test_date_ticker_factory():
1279-
data = (
1280-
(0.02, mdates.MinuteLocator),
1281-
(1, mdates.HourLocator),
1282-
(19, mdates.DayLocator),
1283-
(40, mdates.WeekdayLocator),
1284-
(200, mdates.MonthLocator),
1285-
(2000, mdates.YearLocator),
1286-
)
1287-
for span, expected_locator in data:
1288-
locator, _ = mdates.date_ticker_factory(span)
1289-
assert isinstance(locator, expected_locator)
1319+
@pytest.mark.parametrize('span, expected_locator',
1320+
((0.02, mdates.MinuteLocator),
1321+
(1, mdates.HourLocator),
1322+
(19, mdates.DayLocator),
1323+
(40, mdates.WeekdayLocator),
1324+
(200, mdates.MonthLocator),
1325+
(2000, mdates.YearLocator)))
1326+
def test_date_ticker_factory(span, expected_locator):
1327+
locator, _ = mdates.date_ticker_factory(span)
1328+
assert isinstance(locator, expected_locator)

0 commit comments

Comments
 (0)