diff --git a/boltons/fileutils.py b/boltons/fileutils.py index 4b2577b..f84f83c 100644 --- a/boltons/fileutils.py +++ b/boltons/fileutils.py @@ -371,7 +371,7 @@ def iter_find_files(directory, patterns, ignored=None): >>> filenames = sorted(iter_find_files(_CUR_DIR, '*.py')) >>> filenames[-1].split('/')[-1] - 'tzutils.py' + 'typeutils.py' Or, Python files while ignoring emacs lockfiles: diff --git a/boltons/timeutils.py b/boltons/timeutils.py index 97af70b..8c27538 100644 --- a/boltons/timeutils.py +++ b/boltons/timeutils.py @@ -5,18 +5,25 @@ nontrivial, but thankfully its support is first-class in Python. ``dateutils`` provides some additional tools for working with time. -See :mod:`tzutils` for handy timezone-related boltons. +Additionally, timeutils provides a few basic utilities for working +with timezones in Python. The Python :mod:`datetime` module's +documentation describes how to create a +:class:`datetime.datetime`-compatible :class:`datetime.tzinfo` +subtype. It even provides a few examples. + +The following module defines usable forms of the timezones in those +docs, as well as a couple other useful ones, :data:`UTC` (aka GMT) and +:data:`LocalTZ` (representing the local timezone as configured in the +operating system). For timezones beyond these, as well as a higher +degree of accuracy in corner cases, check out `pytz`_. + +.. _pytz: https://pypi.python.org/pypi/pytz """ - import re +import time import bisect -import datetime -from datetime import timedelta - - -__all__ = ['total_seconds', 'isoparse', 'parse_td', 'relative_time', - 'decimal_relative_time'] +from datetime import tzinfo, timedelta, datetime def total_seconds(td): @@ -28,7 +35,7 @@ def total_seconds(td): Returns: float: total number of seconds - >>> td = datetime.timedelta(days=4, seconds=33) + >>> td = timedelta(days=4, seconds=33) >>> total_seconds(td) 345633.0 """ @@ -42,28 +49,30 @@ _NONDIGIT_RE = re.compile('\D') def isoparse(iso_str): - """Parses the very limited subset of ISO8601 as returned by - :meth:`datetime.datetime.isoformat`. + """Parses the limited subset of `ISO8601-formatted time`_ strings as + returned by :meth:`datetime.datetime.isoformat`. - >>> epoch_dt = datetime.datetime.utcfromtimestamp(0) + >>> epoch_dt = datetime.utcfromtimestamp(0) >>> iso_str = epoch_dt.isoformat() >>> print(iso_str) 1970-01-01T00:00:00 >>> isoparse(iso_str) datetime.datetime(1970, 1, 1, 0, 0) - >>> utcnow = datetime.datetime.utcnow() + >>> utcnow = datetime.utcnow() >>> utcnow == isoparse(utcnow.isoformat()) True For further datetime parsing, see the `iso8601`_ package for strict ISO parsing and `dateutil`_ package for loose parsing and more. + .. _ISO8601-formatted time strings: https://en.wikipedia.org/wiki/ISO_8601 .. _iso8601: https://pypi.python.org/pypi/iso8601 .. _dateutil: https://pypi.python.org/pypi/python-dateutil + """ dt_args = [int(p) for p in _NONDIGIT_RE.split(iso_str)] - return datetime.datetime(*dt_args) + return datetime(*dt_args) _BOUNDS = [(0, timedelta(seconds=1), 'second'), @@ -157,7 +166,7 @@ def decimal_relative_time(d, other=None, ndigits=0, cardinalize=True): localization into other languages and custom phrasing and formatting. - >>> now = datetime.datetime.utcnow() + >>> now = datetime.utcnow() >>> decimal_relative_time(now - timedelta(days=1, seconds=3600), now) (1.0, 'day') >>> decimal_relative_time(now - timedelta(seconds=0.002), now, ndigits=5) @@ -166,7 +175,7 @@ def decimal_relative_time(d, other=None, ndigits=0, cardinalize=True): (-2.5, 'years') """ if other is None: - other = datetime.datetime.utcnow() + other = datetime.utcnow() diff = other - d diff_seconds = total_seconds(diff) abs_diff = abs(diff) @@ -194,7 +203,7 @@ def relative_time(d, other=None, ndigits=0): Returns: A short English-language string. - >>> now = datetime.datetime.utcnow() + >>> now = datetime.utcnow() >>> relative_time(now, ndigits=1) '0 seconds ago' >>> relative_time(now - timedelta(days=1, seconds=36000), ndigits=1) @@ -208,3 +217,184 @@ def relative_time(d, other=None, ndigits=0): if drt < 0: phrase = 'from now' return '%g %s %s' % (abs(drt), unit, phrase) + + +# Timezone support (brought in from tzutils) + + +ZERO = timedelta(0) +HOUR = timedelta(hours=1) + + +class ConstantTZInfo(tzinfo): + """ + A :class:`datetime.tzinfo` subtype whose *offset* remains constant + (no daylight savings). + + Args: + name (str): Name of the timezone. + offset (datetime.timedelta): Offset of the timezone. + """ + def __init__(self, name="ConstantTZ", offset=ZERO): + self.name = name + self.offset = offset + + @property + def utcoffset_hours(self): + return total_seconds(self.offset) / (60 * 60) + + def utcoffset(self, dt): + return self.offset + + def tzname(self, dt): + return self.name + + def dst(self, dt): + return ZERO + + def __repr__(self): + cn = self.__class__.__name__ + return '%s(name=%r, offset=%r)' % (cn, self.name, self.offset) + + +UTC = ConstantTZInfo('UTC') + + +class LocalTZInfo(tzinfo): + """The ``LocalTZInfo`` type takes data available in the time module + about the local timezone and makes a practical + :class:`datetime.tzinfo` to represent the timezone settings of the + operating system. + + For a more in-depth integration with the operating system, check + out `tzlocal`_. It builds on `pytz`_ and implements heuristics for + many versions of major operating systems to provide the official + ``pytz`` tzinfo, instead of the LocalTZ generalization. + + .. _tzlocal: https://pypi.python.org/pypi/tzlocal + .. _pytz: https://pypi.python.org/pypi/pytz + + """ + _std_offset = timedelta(seconds=-time.timezone) + _dst_offset = _std_offset + if time.daylight: + _dst_offset = timedelta(seconds=-time.altzone) + + def is_dst(self, dt): + dt_t = (dt.year, dt.month, dt.day, dt.hour, dt.minute, + dt.second, dt.weekday(), 0, -1) + local_t = time.localtime(time.mktime(dt_t)) + return local_t.tm_isdst > 0 + + def utcoffset(self, dt): + if self.is_dst(dt): + return self._dst_offset + return self._std_offset + + def dst(self, dt): + if self.is_dst(dt): + return self._dst_offset - self._std_offset + return ZERO + + def tzname(self, dt): + return time.tzname[self.is_dst(dt)] + + def __repr__(self): + return '%s()' % self.__class__.__name__ + + +LocalTZ = LocalTZInfo() + + +def _first_sunday_on_or_after(dt): + days_to_go = 6 - dt.weekday() + if days_to_go: + dt += timedelta(days_to_go) + return dt + + +# US DST Rules +# +# This is a simplified (i.e., wrong for a few cases) set of rules for US +# DST start and end times. For a complete and up-to-date set of DST rules +# and timezone definitions, visit the Olson Database (or try pytz): +# http://www.twinsun.com/tz/tz-link.htm +# http://sourceforge.net/projects/pytz/ (might not be up-to-date) +# +# In the US, since 2007, DST starts at 2am (standard time) on the second +# Sunday in March, which is the first Sunday on or after Mar 8. +DSTSTART_2007 = datetime(1, 3, 8, 2) +# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov. +DSTEND_2007 = datetime(1, 11, 1, 1) +# From 1987 to 2006, DST used to start at 2am (standard time) on the first +# Sunday in April and to end at 2am (DST time; 1am standard time) on the last +# Sunday of October, which is the first Sunday on or after Oct 25. +DSTSTART_1987_2006 = datetime(1, 4, 1, 2) +DSTEND_1987_2006 = datetime(1, 10, 25, 1) +# From 1967 to 1986, DST used to start at 2am (standard time) on the last +# Sunday in April (the one on or after April 24) and to end at 2am (DST time; +# 1am standard time) on the last Sunday of October, which is the first Sunday +# on or after Oct 25. +DSTSTART_1967_1986 = datetime(1, 4, 24, 2) +DSTEND_1967_1986 = DSTEND_1987_2006 + + +class USTimeZone(tzinfo): + """Copied directly from the Python docs, the ``USTimeZone`` is a + :class:`datetime.tzinfo` subtype used to create the + :data:`Eastern`, :data:`Central`, :data:`Mountain`, and + :data:`Pacific` tzinfo types. + """ + def __init__(self, hours, reprname, stdname, dstname): + self.stdoffset = timedelta(hours=hours) + self.reprname = reprname + self.stdname = stdname + self.dstname = dstname + + def __repr__(self): + return self.reprname + + def tzname(self, dt): + if self.dst(dt): + return self.dstname + else: + return self.stdname + + def utcoffset(self, dt): + return self.stdoffset + self.dst(dt) + + def dst(self, dt): + if dt is None or dt.tzinfo is None: + # An exception may be sensible here, in one or both cases. + # It depends on how you want to treat them. The default + # fromutc() implementation (called by the default astimezone() + # implementation) passes a datetime with dt.tzinfo is self. + return ZERO + assert dt.tzinfo is self + + # Find start and end times for US DST. For years before 1967, return + # ZERO for no DST. + if 2006 < dt.year: + dststart, dstend = DSTSTART_2007, DSTEND_2007 + elif 1986 < dt.year < 2007: + dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006 + elif 1966 < dt.year < 1987: + dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986 + else: + return ZERO + + start = _first_sunday_on_or_after(dststart.replace(year=dt.year)) + end = _first_sunday_on_or_after(dstend.replace(year=dt.year)) + + # Can't compare naive to aware objects, so strip the timezone + # from dt first. + if start <= dt.replace(tzinfo=None) < end: + return HOUR + else: + return ZERO + + +Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") +Central = USTimeZone(-6, "Central", "CST", "CDT") +Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") +Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") diff --git a/boltons/tzutils.py b/boltons/tzutils.py deleted file mode 100644 index 91370a5..0000000 --- a/boltons/tzutils.py +++ /dev/null @@ -1,206 +0,0 @@ -# -*- coding: utf-8 -*- -"""The Python :mod:`datetime` module's documentation describes how -to create a :class:`datetime.datetime`-compatible -:class:`datetime.tzinfo` subtype. It even provides a few examples. - -The following module defines usable forms of the timezones in those -docs, as well as a couple other useful ones, :data:`UTC` (aka GMT) and -:data:`LocalTZ` (representing the local timezone). For timezones -beyond these, as well as a higher degree of accuracy, check out `pytz`_. - -.. _pytz: https://pypi.python.org/pypi/pytz - -""" - - -import time -from datetime import tzinfo, timedelta, datetime - -# Basic timezones cribbed from etavta and Python docs. - -ZERO = timedelta(0) -HOUR = timedelta(hours=1) - - -# copied from timeutils for tzutils independence: -def total_seconds(td): - """For those with older versions of Python, a pure-Python - implementation of Python 2.7's :meth:`timedelta.total_seconds`. - - Args: - td (datetime.timedelta): The timedelta to convert to seconds. - Returns: - float: total number of seconds - - >>> td = timedelta(days=4, seconds=33) - >>> total_seconds(td) - 345633.0 - """ - a_milli = 1000000.0 - td_ds = td.seconds + (td.days * 86400) # 24 * 60 * 60 - td_micro = td.microseconds + (td_ds * a_milli) - return td_micro / a_milli - - -class ConstantTZInfo(tzinfo): - """ - A :class:`datetime.tzinfo` subtype whose *offset* remains constant - (no daylight savings). - - Args: - name (str): Name of the timezone. - offset (datetime.timedelta): Offset of the timezone. - """ - def __init__(self, name="ConstantTZ", offset=ZERO): - self.name = name - self.offset = offset - - @property - def utcoffset_hours(self): - return total_seconds(self.offset) / (60 * 60) - - def utcoffset(self, dt): - return self.offset - - def tzname(self, dt): - return self.name - - def dst(self, dt): - return ZERO - - def __repr__(self): - cn = self.__class__.__name__ - return '%s(name=%r, offset=%r)' % (cn, self.name, self.offset) - - -UTC = ConstantTZInfo('UTC') - - -class LocalTZInfo(tzinfo): - """The ``LocalTZInfo`` type takes data available in the time module about - the local timezone and makes a practical tzinfo to represent the - timezone settings of the operating system. - """ - _std_offset = timedelta(seconds=-time.timezone) - _dst_offset = _std_offset - if time.daylight: - _dst_offset = timedelta(seconds=-time.altzone) - - def is_dst(self, dt): - dt_t = (dt.year, dt.month, dt.day, dt.hour, dt.minute, - dt.second, dt.weekday(), 0, -1) - local_t = time.localtime(time.mktime(dt_t)) - return local_t.tm_isdst > 0 - - def utcoffset(self, dt): - if self.is_dst(dt): - return self._dst_offset - return self._std_offset - - def dst(self, dt): - if self.is_dst(dt): - return self._dst_offset - self._std_offset - return ZERO - - def tzname(self, dt): - return time.tzname[self.is_dst(dt)] - - def __repr__(self): - return '%s()' % self.__class__.__name__ - - -LocalTZ = LocalTZInfo() - - -def _first_sunday_on_or_after(dt): - days_to_go = 6 - dt.weekday() - if days_to_go: - dt += timedelta(days_to_go) - return dt - - -# US DST Rules -# -# This is a simplified (i.e., wrong for a few cases) set of rules for US -# DST start and end times. For a complete and up-to-date set of DST rules -# and timezone definitions, visit the Olson Database (or try pytz): -# http://www.twinsun.com/tz/tz-link.htm -# http://sourceforge.net/projects/pytz/ (might not be up-to-date) -# -# In the US, since 2007, DST starts at 2am (standard time) on the second -# Sunday in March, which is the first Sunday on or after Mar 8. -DSTSTART_2007 = datetime(1, 3, 8, 2) -# and ends at 2am (DST time; 1am standard time) on the first Sunday of Nov. -DSTEND_2007 = datetime(1, 11, 1, 1) -# From 1987 to 2006, DST used to start at 2am (standard time) on the first -# Sunday in April and to end at 2am (DST time; 1am standard time) on the last -# Sunday of October, which is the first Sunday on or after Oct 25. -DSTSTART_1987_2006 = datetime(1, 4, 1, 2) -DSTEND_1987_2006 = datetime(1, 10, 25, 1) -# From 1967 to 1986, DST used to start at 2am (standard time) on the last -# Sunday in April (the one on or after April 24) and to end at 2am (DST time; -# 1am standard time) on the last Sunday of October, which is the first Sunday -# on or after Oct 25. -DSTSTART_1967_1986 = datetime(1, 4, 24, 2) -DSTEND_1967_1986 = DSTEND_1987_2006 - - -class USTimeZone(tzinfo): - """Copied directly from the Python docs, the ``USTimeZone`` is a - :class:`datetime.tzinfo` subtype used to create the - :data:`Eastern`, :data:`Central`, :data:`Mountain`, and - :data:`Pacific` tzinfo types. - """ - def __init__(self, hours, reprname, stdname, dstname): - self.stdoffset = timedelta(hours=hours) - self.reprname = reprname - self.stdname = stdname - self.dstname = dstname - - def __repr__(self): - return self.reprname - - def tzname(self, dt): - if self.dst(dt): - return self.dstname - else: - return self.stdname - - def utcoffset(self, dt): - return self.stdoffset + self.dst(dt) - - def dst(self, dt): - if dt is None or dt.tzinfo is None: - # An exception may be sensible here, in one or both cases. - # It depends on how you want to treat them. The default - # fromutc() implementation (called by the default astimezone() - # implementation) passes a datetime with dt.tzinfo is self. - return ZERO - assert dt.tzinfo is self - - # Find start and end times for US DST. For years before 1967, return - # ZERO for no DST. - if 2006 < dt.year: - dststart, dstend = DSTSTART_2007, DSTEND_2007 - elif 1986 < dt.year < 2007: - dststart, dstend = DSTSTART_1987_2006, DSTEND_1987_2006 - elif 1966 < dt.year < 1987: - dststart, dstend = DSTSTART_1967_1986, DSTEND_1967_1986 - else: - return ZERO - - start = _first_sunday_on_or_after(dststart.replace(year=dt.year)) - end = _first_sunday_on_or_after(dstend.replace(year=dt.year)) - - # Can't compare naive to aware objects, so strip the timezone - # from dt first. - if start <= dt.replace(tzinfo=None) < end: - return HOUR - else: - return ZERO - - -Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") -Central = USTimeZone(-6, "Central", "CST", "CDT") -Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") -Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") diff --git a/docs/architecture.rst b/docs/architecture.rst index a591c5b..eac8abf 100644 --- a/docs/architecture.rst +++ b/docs/architecture.rst @@ -58,7 +58,7 @@ a definite set of themes have emerged: 1. :mod:`~boltons.queueutils` - `heapq docs`_ 2. :mod:`~boltons.iterutils` - `itertools docs`_ - 3. :mod:`~boltons.tzutils` - `datetime docs`_ + 3. :mod:`~boltons.timeutils` - `datetime docs`_ 2. Reimplementations and tweaks of the standard library: diff --git a/docs/index.rst b/docs/index.rst index 67fe39d..f65ba5c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -104,4 +104,3 @@ Section listing tbutils timeutils typeutils - tzutils diff --git a/docs/timeutils.rst b/docs/timeutils.rst index 1dcae2c..8bd1637 100644 --- a/docs/timeutils.rst +++ b/docs/timeutils.rst @@ -2,5 +2,46 @@ ====================================== .. automodule:: boltons.timeutils - :members: - :undoc-members: + +.. autofunction:: total_seconds +.. autofunction:: isoparse +.. autofunction:: parse_timedelta +.. autofunction:: total_seconds +.. autofunction:: relative_time +.. autofunction:: decimal_relative_time + +General timezones +----------------- + +By default, :class:`datetime.datetime` objects are "naïve", meaning +they lack attached timezone information. These objects can be useful +for many operations, but many operations require timezone-aware +datetimes. + +The two most important timezones in programming are Coordinated +Universal Time (`UTC`_) and the local timezone of the host running +your code. Boltons provides two :class:`datetime.tzinfo` subtypes for +working with them: + +.. _UTC: https://en.wikipedia.org/wiki/Coordinated_Universal_Time + +.. autoattribute:: boltons.timeutils.UTC +.. autodata:: boltons.timeutils.LocalTZ + +.. autoclass:: boltons.timeutils.ConstantTZInfo + +US timezones +------------ + +These four US timezones were implemented in the :mod:`datetime` +documentation and have been reproduced here in boltons for +convenience. More in-depth support is provided by `pytz`_. + +.. _pytz: https://pypi.python.org/pypi/pytz + +.. autoattribute:: boltons.timeutils.Eastern +.. autoattribute:: boltons.timeutils.Central +.. autoattribute:: boltons.timeutils.Mountain +.. autoattribute:: boltons.timeutils.Pacific + +.. autoclass:: boltons.timeutils.USTimeZone diff --git a/docs/tzutils.rst b/docs/tzutils.rst deleted file mode 100644 index c3e1598..0000000 --- a/docs/tzutils.rst +++ /dev/null @@ -1,22 +0,0 @@ -``tzutils`` - Barebone timezones -================================ - -.. automodule:: boltons.tzutils - -General timezones ------------------ - -.. autoattribute:: boltons.tzutils.UTC -.. autodata:: boltons.tzutils.LocalTZ - -.. autoclass:: boltons.tzutils.ConstantTZInfo - -US timezones ------------- - -.. autoattribute:: boltons.tzutils.Eastern -.. autoattribute:: boltons.tzutils.Central -.. autoattribute:: boltons.tzutils.Mountain -.. autoattribute:: boltons.tzutils.Pacific - -.. autoclass:: boltons.tzutils.USTimeZone