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

Skip to content

Commit afb14cb

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

File tree

2 files changed

+111
-65
lines changed

2 files changed

+111
-65
lines changed

lib/matplotlib/dates.py

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,11 @@
8383
Out[1]: 732401
8484
8585
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.
86+
If no explicit timezone is provided, :rc:`timezone` is assumed, provided as a
87+
string. If you want to use a custom timezone, pass a `datetime.tzinfo`
88+
instance or a string with the timezone name with the tz keyword argument to
89+
`num2date`, `.Axis.axis_date`, and any custom date tickers or locators you
90+
create. Timezone 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 timezone 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 timezone 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
"""
@@ -341,8 +354,7 @@ def _from_ordinalf(x, tz=None):
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'))
@@ -508,8 +520,8 @@ 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+
Timezone of *x*. If a string, *tz* is passed to `dateutil.tz`.
513525
514526
Returns
515527
-------
@@ -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`
635+
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
625636
Ticks timezone.
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``
@@ -1000,7 +1009,7 @@ def set(self, **kwargs):
10001009
def _update_rrule(self, **kwargs):
10011010
tzinfo = self._base_tzinfo
10021011

1003-
# rrule does not play nicely with time zones - especially pytz time
1012+
# rrule does not play nicely with timezones - especially pytz time
10041013
# zones, it's best to use naive zones and attach timezones once the
10051014
# datetimes are returned
10061015
if 'dtstart' in kwargs:
@@ -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 timezone 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,7 +1290,7 @@ def __init__(self, tz=None, minticks=5, maxticks=None,
12831290
"""
12841291
Parameters
12851292
----------
1286-
tz : `datetime.tzinfo`
1293+
tz : str or `~datetime.tzinfo`, default: :rc:`timezone`
12871294
Ticks timezone.
12881295
minticks : int
12891296
The minimum number of ticks desired; controls whether ticks occur
@@ -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: 61 additions & 22 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

@@ -976,7 +979,7 @@ def date_range(start, freq, periods):
976979

977980
return [dtstart + (i * freq) for i in range(periods)]
978981

979-
# Define a tz_convert function that converts a list to a new time zone.
982+
# Define a tz_convert function that converts a list to a new timezone.
980983
def tz_convert(dt_list, tzinfo):
981984
return [d.astimezone(tzinfo) for d in dt_list]
982985

@@ -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 timezone"):
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)