From 19c90338999a4facbffa0a92f09a7aa3a2ada159 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 17 Aug 2017 10:39:25 -0700 Subject: [PATCH 1/9] Refactor timezones functions out of tslib timezones.pyx has no other dependencies within pandas, helping to de-tangle some of the _libs modules Code im timezones is used in both tslib and period, and a bit in index.pyx This is one of several steps in making _libs.period not need to import from non-cython code and ideally not need to import tslib (though NaT makes that tough). See existing comments in _libs.__init__ on why this is desireable. This is the first of several independent pieces to be split off of tslib. Cleanup commented-out code --- pandas/_libs/index.pyx | 7 +- pandas/_libs/period.pyx | 48 ++--- pandas/_libs/tslib.pxd | 4 - pandas/_libs/tslib.pyx | 290 ++++----------------------- pandas/_libs/tslibs/__init__.py | 3 + pandas/_libs/tslibs/timezones.pxd | 22 +++ pandas/_libs/tslibs/timezones.pyx | 316 ++++++++++++++++++++++++++++++ setup.py | 12 +- 8 files changed, 416 insertions(+), 286 deletions(-) create mode 100644 pandas/_libs/tslibs/__init__.py create mode 100644 pandas/_libs/tslibs/timezones.pxd create mode 100644 pandas/_libs/tslibs/timezones.pyx diff --git a/pandas/_libs/index.pyx b/pandas/_libs/index.pyx index 42ba0c1cadaec..7e48c7d94ccf0 100644 --- a/pandas/_libs/index.pyx +++ b/pandas/_libs/index.pyx @@ -24,6 +24,8 @@ from datetime import datetime, timedelta from datetime cimport (get_datetime64_value, _pydatetime_to_dts, pandas_datetimestruct) +from tslibs.timezones cimport _get_utcoffset, _is_utc + from cpython cimport PyTuple_Check, PyList_Check cdef extern from "datetime.h": @@ -554,14 +556,11 @@ cdef inline _to_i8(object val): # Save the original date value so we can get the utcoffset from it. ival = _pydatetime_to_dts(val, &dts) if tzinfo is not None and not _is_utc(tzinfo): - offset = tslib._get_utcoffset(tzinfo, val) + offset = _get_utcoffset(tzinfo, val) ival -= tslib._delta_to_nanoseconds(offset) return ival return val -cdef inline bint _is_utc(object tz): - return tz is UTC or isinstance(tz, _du_utc) - cdef class MultiIndexObjectEngine(ObjectEngine): """ diff --git a/pandas/_libs/period.pyx b/pandas/_libs/period.pyx index e017d863e1907..fc8616a4bf444 100644 --- a/pandas/_libs/period.pyx +++ b/pandas/_libs/period.pyx @@ -10,19 +10,19 @@ from numpy cimport (int8_t, int32_t, int64_t, import_array, ndarray, NPY_INT64, NPY_DATETIME, NPY_TIMEDELTA) import numpy as np -cdef extern from "datetime_helper.h": - double total_seconds(object) - from libc.stdlib cimport free from pandas import compat from pandas.compat import PY2 +from pandas.core.dtypes.generic import ABCDateOffset cimport cython +cdef extern from "datetime.h": + void PyDateTime_IMPORT() + from datetime cimport ( is_leapyear, - PyDateTime_IMPORT, pandas_datetimestruct, pandas_datetimestruct_to_datetime, pandas_datetime_to_datetimestruct, @@ -32,16 +32,17 @@ from datetime cimport ( cimport util, lib from lib cimport is_null_datetimelike, is_period from pandas._libs import tslib, lib -from pandas._libs.tslib import (Timedelta, Timestamp, iNaT, - NaT, _get_utcoffset) -from tslib cimport ( - maybe_get_tz, - _is_utc, - _is_tzlocal, +from pandas._libs.tslib import Timestamp, iNaT, NaT +from tslib cimport _nat_scalar_rules + +from tslibs.timezones cimport ( + total_seconds, + _get_utcoffset, _get_dst_info, - _nat_scalar_rules) + _is_tzlocal, + _is_utc, + maybe_get_tz) -from pandas.tseries import offsets from pandas.core.tools.datetimes import parse_time_string from pandas.tseries import frequencies @@ -117,9 +118,16 @@ cdef extern from "period_helper.h": initialize_daytime_conversion_factor_matrix() + +cpdef _is_tick(item): + # offsets.Tick subclasses offsets.DateOffset and has a "_inc" attribute + return isinstance(item, ABCDateOffset) and hasattr(item, "_inc") + + # Period logic #---------------------------------------------------------------------- +# TODO: never used? cdef inline int64_t apply_mult(int64_t period_ord, int64_t mult): """ Get freq+multiple ordinal value from corresponding freq-only ordinal value. @@ -131,6 +139,7 @@ cdef inline int64_t apply_mult(int64_t period_ord, int64_t mult): return (period_ord - 1) // mult +# TODO: never used? cdef inline int64_t remove_mult(int64_t period_ord_w_mult, int64_t mult): """ Get freq-only ordinal value from corresponding freq+multiple ordinal. @@ -746,10 +755,9 @@ cdef class _Period(object): return hash((self.ordinal, self.freqstr)) def _add_delta(self, other): - if isinstance(other, (timedelta, np.timedelta64, - offsets.Tick, Timedelta)): + if isinstance(other, (timedelta, np.timedelta64)) or _is_tick(other): offset = frequencies.to_offset(self.freq.rule_code) - if isinstance(offset, offsets.Tick): + if _is_tick(offset): nanos = tslib._delta_to_nanoseconds(other) offset_nanos = tslib._delta_to_nanoseconds(offset) @@ -758,7 +766,7 @@ cdef class _Period(object): return Period(ordinal=ordinal, freq=self.freq) msg = 'Input cannot be converted to Period(freq={0})' raise IncompatibleFrequency(msg.format(self.freqstr)) - elif isinstance(other, offsets.DateOffset): + elif isinstance(other, ABCDateOffset): freqstr = other.rule_code base = frequencies.get_base_alias(freqstr) if base == self.freq.rule_code: @@ -771,9 +779,7 @@ cdef class _Period(object): def __add__(self, other): if isinstance(self, Period): - if isinstance(other, (timedelta, np.timedelta64, - offsets.Tick, offsets.DateOffset, - Timedelta)): + if isinstance(other, (timedelta, np.timedelta64, ABCDateOffset)): return self._add_delta(other) elif other is NaT: return NaT @@ -789,9 +795,7 @@ cdef class _Period(object): def __sub__(self, other): if isinstance(self, Period): - if isinstance(other, (timedelta, np.timedelta64, - offsets.Tick, offsets.DateOffset, - Timedelta)): + if isinstance(other, (timedelta, np.timedelta64, ABCDateOffset)): neg_other = -other return self + neg_other elif lib.is_integer(other): diff --git a/pandas/_libs/tslib.pxd b/pandas/_libs/tslib.pxd index aa8cbcb2cedc7..ee8adfe67bb5e 100644 --- a/pandas/_libs/tslib.pxd +++ b/pandas/_libs/tslib.pxd @@ -2,9 +2,5 @@ from numpy cimport ndarray, int64_t cdef convert_to_tsobject(object, object, object, bint, bint) cpdef convert_to_timedelta64(object, object) -cpdef object maybe_get_tz(object) -cdef bint _is_utc(object) -cdef bint _is_tzlocal(object) -cdef object _get_dst_info(object) cdef bint _nat_scalar_rules[6] cdef bint _check_all_nulls(obj) diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index 32b8c92a50269..f7c62bec84d69 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -26,9 +26,6 @@ from cpython cimport ( cdef extern from "Python.h": cdef PyTypeObject *Py_TYPE(object) -cdef extern from "datetime_helper.h": - double total_seconds(object) - # this is our datetime.pxd from libc.stdlib cimport free @@ -36,6 +33,13 @@ from util cimport (is_integer_object, is_float_object, is_datetime64_object, is_timedelta64_object, INT64_MAX) cimport util +cdef extern from "datetime.h": + bint PyDateTime_Check(object o) + bint PyDate_Check(object o) + void PyDateTime_IMPORT() + +from datetime cimport datetime, timedelta + # this is our datetime.pxd from datetime cimport ( pandas_datetimestruct, @@ -53,12 +57,35 @@ from datetime cimport ( npy_datetime, is_leapyear, dayofweek, - PANDAS_FR_ns, - PyDateTime_Check, PyDate_Check, - PyDateTime_IMPORT, - timedelta, datetime + PANDAS_FR_ns ) +from tslibs.timezones import ( + tzoffset, + _dateutil_gettz, + _dateutil_tzlocal, + _dateutil_tzfile, + _dateutil_tzutc, + maybe_get_tz, + _get_utcoffset, + _unbox_utcoffsets, + get_timezone, + _p_tz_cache_key) +from tslibs.timezones cimport ( + total_seconds, + _is_utc, + maybe_get_tz, + _is_tzlocal, + _get_dst_info, + _get_utcoffset, + _unbox_utcoffsets, + _is_fixed_offset, + _get_zone, + _get_utc_trans_times_from_dateutil_tz, + _tz_cache_key, + _treat_tz_as_pytz, + _treat_tz_as_dateutil) + # stdlib datetime imports from datetime import timedelta, datetime from datetime import time as datetime_time @@ -74,26 +101,16 @@ cimport cython import re # dateutil compat -from dateutil.tz import (tzoffset, tzlocal as _dateutil_tzlocal, - tzfile as _dateutil_tzfile, - tzutc as _dateutil_tzutc, - tzstr as _dateutil_tzstr) - -from pandas.compat import is_platform_windows -if is_platform_windows(): - from dateutil.zoneinfo import gettz as _dateutil_gettz -else: - from dateutil.tz import gettz as _dateutil_gettz +from dateutil.tz import tzstr as _dateutil_tzstr + from dateutil.relativedelta import relativedelta from dateutil.parser import DEFAULTPARSER -from pytz.tzinfo import BaseTzInfo as _pytz_BaseTzInfo from pandas.compat import (parse_date, string_types, iteritems, StringIO, callable) import operator import collections -import warnings # initialize numpy import_array() @@ -235,24 +252,6 @@ def ints_to_pytimedelta(ndarray[int64_t] arr, box=False): return result -cdef inline bint _is_tzlocal(object tz): - return isinstance(tz, _dateutil_tzlocal) - - -cdef inline bint _is_fixed_offset(object tz): - if _treat_tz_as_dateutil(tz): - if len(tz._trans_idx) == 0 and len(tz._trans_list) == 0: - return 1 - else: - return 0 - elif _treat_tz_as_pytz(tz): - if (len(tz._transition_info) == 0 - and len(tz._utc_transition_times) == 0): - return 1 - else: - return 0 - return 1 - _zero_time = datetime_time(0, 0) _no_input = object() @@ -1009,6 +1008,7 @@ def unique_deltas(ndarray[int64_t] arr): return result +# TODO: never used? an identical function is defined in tseries.frequencies cdef inline bint _is_multiple(int64_t us, int64_t mult): return us % mult == 0 @@ -1437,11 +1437,6 @@ cdef class _TSObject: def __get__(self): return self.value -cpdef _get_utcoffset(tzinfo, obj): - try: - return tzinfo._utcoffset - except AttributeError: - return tzinfo.utcoffset(obj) # helper to extract datetime and int64 from several different possibilities cdef convert_to_tsobject(object ts, object tz, object unit, @@ -1688,7 +1683,6 @@ cdef inline void _localize_tso(_TSObject obj, object tz): else: obj.tzinfo = tz - def _localize_pydatetime(object dt, object tz): """ Take a datetime/Timestamp in UTC and localizes to timezone tz. @@ -1706,71 +1700,6 @@ def _localize_pydatetime(object dt, object tz): return dt.replace(tzinfo=tz) -def get_timezone(tz): - return _get_zone(tz) - -cdef inline bint _is_utc(object tz): - return tz is UTC or isinstance(tz, _dateutil_tzutc) - -cdef inline object _get_zone(object tz): - """ - We need to do several things here: - 1) Distinguish between pytz and dateutil timezones - 2) Not be over-specific (e.g. US/Eastern with/without DST is same *zone* - but a different tz object) - 3) Provide something to serialize when we're storing a datetime object - in pytables. - - We return a string prefaced with dateutil if it's a dateutil tz, else just - the tz name. It needs to be a string so that we can serialize it with - UJSON/pytables. maybe_get_tz (below) is the inverse of this process. - """ - if _is_utc(tz): - return 'UTC' - else: - if _treat_tz_as_dateutil(tz): - if '.tar.gz' in tz._filename: - raise ValueError( - 'Bad tz filename. Dateutil on python 3 on windows has a ' - 'bug which causes tzfile._filename to be the same for all ' - 'timezone files. Please construct dateutil timezones ' - 'implicitly by passing a string like "dateutil/Europe' - '/London" when you construct your pandas objects instead ' - 'of passing a timezone object. See ' - 'https://github.com/pandas-dev/pandas/pull/7362') - return 'dateutil/' + tz._filename - else: - # tz is a pytz timezone or unknown. - try: - zone = tz.zone - if zone is None: - return tz - return zone - except AttributeError: - return tz - - -cpdef inline object maybe_get_tz(object tz): - """ - (Maybe) Construct a timezone object from a string. If tz is a string, use - it to construct a timezone object. Otherwise, just return tz. - """ - if isinstance(tz, string_types): - if tz == 'tzlocal()': - tz = _dateutil_tzlocal() - elif tz.startswith('dateutil/'): - zone = tz[9:] - tz = _dateutil_gettz(zone) - # On Python 3 on Windows, the filename is not always set correctly. - if isinstance(tz, _dateutil_tzfile) and '.tar.gz' in tz._filename: - tz._filename = zone - else: - tz = pytz.timezone(tz) - elif is_integer_object(tz): - tz = pytz.FixedOffset(tz / 60) - return tz - - class OutOfBoundsDatetime(ValueError): pass @@ -3960,7 +3889,7 @@ for _maybe_method_name in dir(NaTType): def f(*args, **kwargs): raise ValueError("NaTType does not support " + func_name) f.__name__ = func_name - f.__doc__ = _get_docstring(_method_name) + f.__doc__ = _get_docstring(func_name) return f setattr(NaTType, _maybe_method_name, @@ -4089,6 +4018,7 @@ def pydt_to_i8(object pydt): return ts.value +# TODO: Never used? def i8_to_pydt(int64_t i8, object tzinfo = None): """ Inverse of pydt_to_i8 @@ -4279,151 +4209,10 @@ def tz_convert_single(int64_t val, object tz1, object tz2): offset = deltas[pos] return utc_date + offset -# Timezone data caches, key is the pytz string or dateutil file name. -dst_cache = {} - -cdef inline bint _treat_tz_as_pytz(object tz): - return hasattr(tz, '_utc_transition_times') and hasattr( - tz, '_transition_info') - -cdef inline bint _treat_tz_as_dateutil(object tz): - return hasattr(tz, '_trans_list') and hasattr(tz, '_trans_idx') - - -def _p_tz_cache_key(tz): - """ Python interface for cache function to facilitate testing.""" - return _tz_cache_key(tz) - - -cdef inline object _tz_cache_key(object tz): - """ - Return the key in the cache for the timezone info object or None - if unknown. - - The key is currently the tz string for pytz timezones, the filename for - dateutil timezones. - - Notes - ===== - This cannot just be the hash of a timezone object. Unfortunately, the - hashes of two dateutil tz objects which represent the same timezone are - not equal (even though the tz objects will compare equal and represent - the same tz file). Also, pytz objects are not always hashable so we use - str(tz) instead. - """ - if isinstance(tz, _pytz_BaseTzInfo): - return tz.zone - elif isinstance(tz, _dateutil_tzfile): - if '.tar.gz' in tz._filename: - raise ValueError('Bad tz filename. Dateutil on python 3 on ' - 'windows has a bug which causes tzfile._filename ' - 'to be the same for all timezone files. Please ' - 'construct dateutil timezones implicitly by ' - 'passing a string like "dateutil/Europe/London" ' - 'when you construct your pandas objects instead ' - 'of passing a timezone object. See ' - 'https://github.com/pandas-dev/pandas/pull/7362') - return 'dateutil' + tz._filename - else: - return None - - -cdef object _get_dst_info(object tz): - """ - return a tuple of : - (UTC times of DST transitions, - UTC offsets in microseconds corresponding to DST transitions, - string of type of transitions) - - """ - cache_key = _tz_cache_key(tz) - if cache_key is None: - num = int(total_seconds(_get_utcoffset(tz, None))) * 1000000000 - return (np.array([NPY_NAT + 1], dtype=np.int64), - np.array([num], dtype=np.int64), - None) - - if cache_key not in dst_cache: - if _treat_tz_as_pytz(tz): - trans = np.array(tz._utc_transition_times, dtype='M8[ns]') - trans = trans.view('i8') - try: - if tz._utc_transition_times[0].year == 1: - trans[0] = NPY_NAT + 1 - except Exception: - pass - deltas = _unbox_utcoffsets(tz._transition_info) - typ = 'pytz' - - elif _treat_tz_as_dateutil(tz): - if len(tz._trans_list): - # get utc trans times - trans_list = _get_utc_trans_times_from_dateutil_tz(tz) - trans = np.hstack([ - np.array([0], dtype='M8[s]'), # place holder for first item - np.array(trans_list, dtype='M8[s]')]).astype( - 'M8[ns]') # all trans listed - trans = trans.view('i8') - trans[0] = NPY_NAT + 1 - - # deltas - deltas = np.array([v.offset for v in ( - tz._ttinfo_before,) + tz._trans_idx], dtype='i8') - deltas *= 1000000000 - typ = 'dateutil' - - elif _is_fixed_offset(tz): - trans = np.array([NPY_NAT + 1], dtype=np.int64) - deltas = np.array([tz._ttinfo_std.offset], - dtype='i8') * 1000000000 - typ = 'fixed' - else: - trans = np.array([], dtype='M8[ns]') - deltas = np.array([], dtype='i8') - typ = None - - else: - # static tzinfo - trans = np.array([NPY_NAT + 1], dtype=np.int64) - num = int(total_seconds(_get_utcoffset(tz, None))) * 1000000000 - deltas = np.array([num], dtype=np.int64) - typ = 'static' - - dst_cache[cache_key] = (trans, deltas, typ) - - return dst_cache[cache_key] - -cdef object _get_utc_trans_times_from_dateutil_tz(object tz): - """ - Transition times in dateutil timezones are stored in local non-dst - time. This code converts them to UTC. It's the reverse of the code - in dateutil.tz.tzfile.__init__. - """ - new_trans = list(tz._trans_list) - last_std_offset = 0 - for i, (trans, tti) in enumerate(zip(tz._trans_list, tz._trans_idx)): - if not tti.isdst: - last_std_offset = tti.offset - new_trans[i] = trans - last_std_offset - return new_trans - def tot_seconds(td): return total_seconds(td) -cpdef ndarray _unbox_utcoffsets(object transinfo): - cdef: - Py_ssize_t i, sz - ndarray[int64_t] arr - - sz = len(transinfo) - arr = np.empty(sz, dtype='i8') - - for i in range(sz): - arr[i] = int(total_seconds(transinfo[i][0])) * 1000000000 - - return arr - @cython.boundscheck(False) @cython.wraparound(False) @@ -5143,6 +4932,7 @@ def get_date_name_field(ndarray[int64_t] dtindex, object field): raise ValueError("Field %s not supported" % field) +# TODO: never used? cdef inline int m8_weekday(int64_t val): ts = convert_to_tsobject(val, None, None, 0, 0) return ts_dayofweek(ts) diff --git a/pandas/_libs/tslibs/__init__.py b/pandas/_libs/tslibs/__init__.py new file mode 100644 index 0000000000000..6ffc2a84242e8 --- /dev/null +++ b/pandas/_libs/tslibs/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# cython: profile=False diff --git a/pandas/_libs/tslibs/timezones.pxd b/pandas/_libs/tslibs/timezones.pxd new file mode 100644 index 0000000000000..0a39454fd794b --- /dev/null +++ b/pandas/_libs/tslibs/timezones.pxd @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# cython: profile=False + +from numpy cimport ndarray, float64_t + +cdef object _get_zone(object tz) +cdef object _tz_cache_key(object tz) +cdef bint _is_utc(object tz) +cdef bint _is_tzlocal(object tz) +cdef bint _treat_tz_as_pytz(object tz) +cdef bint _treat_tz_as_dateutil(object tz) +cpdef object maybe_get_tz(object tz) + +cpdef _get_utcoffset(tzinfo, obj) +cpdef ndarray _unbox_utcoffsets(object transinfo) +cdef bint _is_fixed_offset(object tz) +cdef object _get_utc_trans_times_from_dateutil_tz(object tz) + +cpdef object _get_dst_info(object tz) + +cdef float64_t total_seconds(object td) diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx new file mode 100644 index 0000000000000..b08b2de471be5 --- /dev/null +++ b/pandas/_libs/tslibs/timezones.pyx @@ -0,0 +1,316 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# cython: profile=False + +try: string_types = basestring +except NameError: string_types = str + + +cdef extern from "Python.h": + Py_ssize_t PY_SSIZE_T_MAX + +cdef extern from "datetime.h": + void PyDateTime_IMPORT() + +# import datetime C API +PyDateTime_IMPORT + + +import numpy as np +cimport numpy as np +from numpy cimport ndarray, int64_t, float64_t +np.import_array() + + +# dateutil compat +from dateutil.tz import (tzoffset, + tzlocal as _dateutil_tzlocal, + tzfile as _dateutil_tzfile, + tzutc as _dateutil_tzutc, + tzstr as _dateutil_tzstr) + +import sys +if sys.platform == 'win32' or sys.platform == 'cygwin': + # equiv pd.compat.is_platform_windows() + from dateutil.zoneinfo import gettz as _dateutil_gettz +else: + from dateutil.tz import gettz as _dateutil_gettz + + +from pytz.tzinfo import BaseTzInfo as _pytz_BaseTzInfo +import pytz +UTC = pytz.utc + + +from cpython cimport PyBool_Check +cdef extern from "numpy/ndarrayobject.h": + bint PyArray_IsIntegerScalar(object) + +cdef inline bint is_integer_object(object obj): + # Ported from util (which gets it from numpy_helper) to avoid + # direct dependency + return (not PyBool_Check(obj)) and PyArray_IsIntegerScalar(obj) + + +cdef int64_t NPY_NAT = np.datetime64('nat').astype(np.int64) + + +# TODO: Does this belong somewhere else? +cdef float64_t total_seconds(object td): + # Note: This is marginally faster than the version in datetime_helper. + cdef int64_t microseconds, seconds, days, days_in_seconds + + microseconds = getattr(td, "microseconds", 0) + seconds = getattr(td, "seconds", 0) + days = getattr(td, "days", 0) + days_in_seconds = days * 24 * 3600; + return (microseconds + (seconds + days_in_seconds) * 1000000.0) / 1000000.0 + + +#---------------------------------------------------------------------- +# time zone conversion helpers + +def get_timezone(tz): + return _get_zone(tz) + + +cdef bint _is_utc(object tz): + return tz is UTC or isinstance(tz, _dateutil_tzutc) + + +cdef bint _is_tzlocal(object tz): + return isinstance(tz, _dateutil_tzlocal) + + +cdef bint _treat_tz_as_pytz(object tz): + return (hasattr(tz, '_utc_transition_times') and + hasattr(tz, '_transition_info')) + + +cdef bint _treat_tz_as_dateutil(object tz): + return hasattr(tz, '_trans_list') and hasattr(tz, '_trans_idx') + + +cdef object _get_zone(object tz): + """ + We need to do several things here: + 1) Distinguish between pytz and dateutil timezones + 2) Not be over-specific (e.g. US/Eastern with/without DST is same *zone* + but a different tz object) + 3) Provide something to serialize when we're storing a datetime object + in pytables. + + We return a string prefaced with dateutil if it's a dateutil tz, else just + the tz name. It needs to be a string so that we can serialize it with + UJSON/pytables. maybe_get_tz (below) is the inverse of this process. + """ + if _is_utc(tz): + return 'UTC' + else: + if _treat_tz_as_dateutil(tz): + if '.tar.gz' in tz._filename: + raise ValueError( + 'Bad tz filename. Dateutil on python 3 on windows has a ' + 'bug which causes tzfile._filename to be the same for all ' + 'timezone files. Please construct dateutil timezones ' + 'implicitly by passing a string like "dateutil/Europe' + '/London" when you construct your pandas objects instead ' + 'of passing a timezone object. See ' + 'https://github.com/pandas-dev/pandas/pull/7362') + return 'dateutil/' + tz._filename + # TODO: use os.path.join? + else: + # tz is a pytz timezone or unknown. + try: + zone = tz.zone + if zone is None: + return tz + return zone + except AttributeError: + return tz + + +cpdef object maybe_get_tz(object tz): + """ + (Maybe) Construct a timezone object from a string. If tz is a string, use + it to construct a timezone object. Otherwise, just return tz. + """ + if isinstance(tz, string_types): + if tz == 'tzlocal()': + tz = _dateutil_tzlocal() + elif tz.startswith('dateutil/'): + zone = tz[9:] + tz = _dateutil_gettz(zone) + # On Python 3 on Windows, the filename is not always set correctly. + if isinstance(tz, _dateutil_tzfile) and '.tar.gz' in tz._filename: + tz._filename = zone + else: + tz = pytz.timezone(tz) + elif is_integer_object(tz): + tz = pytz.FixedOffset(tz / 60) + return tz + + +def _p_tz_cache_key(tz): + """ Python interface for cache function to facilitate testing.""" + return _tz_cache_key(tz) + + +cdef object _tz_cache_key(object tz): + """ + Return the key in the cache for the timezone info object or None + if unknown. + + The key is currently the tz string for pytz timezones, the filename for + dateutil timezones. + + Notes + ===== + This cannot just be the hash of a timezone object. Unfortunately, the + hashes of two dateutil tz objects which represent the same timezone are + not equal (even though the tz objects will compare equal and represent + the same tz file). Also, pytz objects are not always hashable so we use + str(tz) instead. + """ + if isinstance(tz, _pytz_BaseTzInfo): + return tz.zone + elif isinstance(tz, _dateutil_tzfile): + if '.tar.gz' in tz._filename: + raise ValueError('Bad tz filename. Dateutil on python 3 on ' + 'windows has a bug which causes tzfile._filename ' + 'to be the same for all timezone files. Please ' + 'construct dateutil timezones implicitly by ' + 'passing a string like "dateutil/Europe/London" ' + 'when you construct your pandas objects instead ' + 'of passing a timezone object. See ' + 'https://github.com/pandas-dev/pandas/pull/7362') + return 'dateutil' + tz._filename + else: + return None + + +#---------------------------------------------------------------------- +# UTC Offsets + +cpdef _get_utcoffset(tzinfo, obj): + try: + return tzinfo._utcoffset + except AttributeError: + return tzinfo.utcoffset(obj) + + +cpdef ndarray _unbox_utcoffsets(object transinfo): + cdef: + Py_ssize_t i, sz + ndarray[int64_t] arr + + sz = len(transinfo) + arr = np.empty(sz, dtype='i8') + + for i in range(sz): + arr[i] = int(total_seconds(transinfo[i][0])) * 1000000000 + + return arr + + +cdef bint _is_fixed_offset(object tz): + if _treat_tz_as_dateutil(tz): + if len(tz._trans_idx) == 0 and len(tz._trans_list) == 0: + return 1 + else: + return 0 + elif _treat_tz_as_pytz(tz): + if (len(tz._transition_info) == 0 + and len(tz._utc_transition_times) == 0): + return 1 + else: + return 0 + return 1 + + +cdef object _get_utc_trans_times_from_dateutil_tz(object tz): + """ + Transition times in dateutil timezones are stored in local non-dst + time. This code converts them to UTC. It's the reverse of the code + in dateutil.tz.tzfile.__init__. + """ + new_trans = list(tz._trans_list) + last_std_offset = 0 + for i, (trans, tti) in enumerate(zip(tz._trans_list, tz._trans_idx)): + if not tti.isdst: + last_std_offset = tti.offset + new_trans[i] = trans - last_std_offset + return new_trans + + +#---------------------------------------------------------------------- +# Daylight Savings + +# Timezone data caches, key is the pytz string or dateutil file name. +dst_cache = {} + +# TODO: go back to just cdef +cpdef object _get_dst_info(object tz): + """ + return a tuple of : + (UTC times of DST transitions, + UTC offsets in microseconds corresponding to DST transitions, + string of type of transitions) + + """ + cache_key = _tz_cache_key(tz) + if cache_key is None: + num = int(total_seconds(_get_utcoffset(tz, None))) * 1000000000 + return (np.array([NPY_NAT + 1], dtype=np.int64), + np.array([num], dtype=np.int64), + None) + + if cache_key not in dst_cache: + if _treat_tz_as_pytz(tz): + trans = np.array(tz._utc_transition_times, dtype='M8[ns]') + trans = trans.view('i8') + try: + if tz._utc_transition_times[0].year == 1: + trans[0] = NPY_NAT + 1 + except Exception: + pass + deltas = _unbox_utcoffsets(tz._transition_info) + typ = 'pytz' + + elif _treat_tz_as_dateutil(tz): + if len(tz._trans_list): + # get utc trans times + trans_list = _get_utc_trans_times_from_dateutil_tz(tz) + trans = np.hstack([ + np.array([0], dtype='M8[s]'), # place holder for first item + np.array(trans_list, dtype='M8[s]')]).astype( + 'M8[ns]') # all trans listed + trans = trans.view('i8') + trans[0] = NPY_NAT + 1 + + # deltas + deltas = np.array([v.offset for v in ( + tz._ttinfo_before,) + tz._trans_idx], dtype='i8') + deltas *= 1000000000 + typ = 'dateutil' + + elif _is_fixed_offset(tz): + trans = np.array([NPY_NAT + 1], dtype=np.int64) + deltas = np.array([tz._ttinfo_std.offset], + dtype='i8') * 1000000000 + typ = 'fixed' + else: + trans = np.array([], dtype='M8[ns]') + deltas = np.array([], dtype='i8') + typ = None + + else: + # static tzinfo + trans = np.array([NPY_NAT + 1], dtype=np.int64) + num = int(total_seconds(_get_utcoffset(tz, None))) * 1000000000 + deltas = np.array([num], dtype=np.int64) + typ = 'static' + + dst_cache[cache_key] = (trans, deltas, typ) + + return dst_cache[cache_key] diff --git a/setup.py b/setup.py index a912b25328954..45ecbddfa7dc1 100755 --- a/setup.py +++ b/setup.py @@ -341,6 +341,7 @@ class CheckSDist(sdist_class): 'pandas/_libs/window.pyx', 'pandas/_libs/sparse.pyx', 'pandas/_libs/parsers.pyx', + 'pandas/_libs/tslibs/timezones.pyx', 'pandas/io/sas/sas.pyx'] def initialize_options(self): @@ -467,7 +468,6 @@ def pxd(name): tseries_depends = ['pandas/_libs/src/datetime/np_datetime.h', 'pandas/_libs/src/datetime/np_datetime_strings.h', - 'pandas/_libs/src/datetime_helper.h', 'pandas/_libs/src/period_helper.h', 'pandas/_libs/src/datetime.pxd'] @@ -484,20 +484,19 @@ def pxd(name): + _pxi_dep['hashtable'])}, '_libs.tslib': {'pyxfile': '_libs/tslib', 'pxdfiles': ['_libs/src/util', '_libs/lib'], - 'depends': tseries_depends, + 'depends': tseries_depends+['pandas/_libs/tslibs/timezones.pyx'], 'sources': ['pandas/_libs/src/datetime/np_datetime.c', 'pandas/_libs/src/datetime/np_datetime_strings.c', 'pandas/_libs/src/period_helper.c']}, '_libs.period': {'pyxfile': '_libs/period', - 'depends': tseries_depends, + 'depends': tseries_depends+['pandas/_libs/tslibs/timezones.pyx'], 'sources': ['pandas/_libs/src/datetime/np_datetime.c', - 'pandas/_libs/src/datetime/np_datetime_strings.c', - 'pandas/_libs/src/period_helper.c']}, + 'pandas/_libs/src/datetime/np_datetime_strings.c']}, '_libs.index': {'pyxfile': '_libs/index', 'sources': ['pandas/_libs/src/datetime/np_datetime.c', 'pandas/_libs/src/datetime/np_datetime_strings.c'], 'pxdfiles': ['_libs/src/util', '_libs/hashtable'], - 'depends': _pxi_dep['index']}, + 'depends': _pxi_dep['index']+['pandas/_libs/tslibs/timezones.pyx']}, '_libs.algos': {'pyxfile': '_libs/algos', 'pxdfiles': ['_libs/src/util', '_libs/algos', '_libs/hashtable'], 'depends': _pxi_dep['algos']}, @@ -529,6 +528,7 @@ def pxd(name): 'depends': ['pandas/_libs/testing.pyx']}, '_libs.hashing': {'pyxfile': '_libs/hashing', 'depends': ['pandas/_libs/hashing.pyx']}, + '_libs.tslibs.timezones': {'pyxfile': '_libs/tslibs/timezones'}, 'io.sas._sas': {'pyxfile': 'io/sas/sas'}, } From b0e36f32294aaf2a1161dad1aac2e34886f5aa82 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 17 Aug 2017 13:43:19 -0700 Subject: [PATCH 2/9] flake8 fixup --- setup.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 45ecbddfa7dc1..99f47c0bacfe3 100755 --- a/setup.py +++ b/setup.py @@ -484,19 +484,22 @@ def pxd(name): + _pxi_dep['hashtable'])}, '_libs.tslib': {'pyxfile': '_libs/tslib', 'pxdfiles': ['_libs/src/util', '_libs/lib'], - 'depends': tseries_depends+['pandas/_libs/tslibs/timezones.pyx'], + 'depends': tseries_depends + \ + ['pandas/_libs/tslibs/timezones.pyx'], 'sources': ['pandas/_libs/src/datetime/np_datetime.c', 'pandas/_libs/src/datetime/np_datetime_strings.c', 'pandas/_libs/src/period_helper.c']}, '_libs.period': {'pyxfile': '_libs/period', - 'depends': tseries_depends+['pandas/_libs/tslibs/timezones.pyx'], + 'depends': tseries_depends + \ + ['pandas/_libs/tslibs/timezones.pyx'], 'sources': ['pandas/_libs/src/datetime/np_datetime.c', 'pandas/_libs/src/datetime/np_datetime_strings.c']}, '_libs.index': {'pyxfile': '_libs/index', 'sources': ['pandas/_libs/src/datetime/np_datetime.c', 'pandas/_libs/src/datetime/np_datetime_strings.c'], 'pxdfiles': ['_libs/src/util', '_libs/hashtable'], - 'depends': _pxi_dep['index']+['pandas/_libs/tslibs/timezones.pyx']}, + 'depends': _pxi_dep['index'] + \ + ['pandas/_libs/tslibs/timezones.pyx']}, '_libs.algos': {'pyxfile': '_libs/algos', 'pxdfiles': ['_libs/src/util', '_libs/algos', '_libs/hashtable'], 'depends': _pxi_dep['algos']}, From c78a23f7775b6d79c99c1e5048cd6fc658392d82 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 19 Aug 2017 11:23:16 -0700 Subject: [PATCH 3/9] flake8 whitespace fixup --- pandas/_libs/tslib.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index f7c62bec84d69..fbd13b9cbd100 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -1683,6 +1683,7 @@ cdef inline void _localize_tso(_TSObject obj, object tz): else: obj.tzinfo = tz + def _localize_pydatetime(object dt, object tz): """ Take a datetime/Timestamp in UTC and localizes to timezone tz. From 0930ed5b61dc692a9371f7682f4fe008ef54a34f Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 23 Aug 2017 11:42:23 -0700 Subject: [PATCH 4/9] revert to using offsets.DateOffset instead of ABCDateOffset --- pandas/_libs/period.pyx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pandas/_libs/period.pyx b/pandas/_libs/period.pyx index fc8616a4bf444..50c829d1dbcc6 100644 --- a/pandas/_libs/period.pyx +++ b/pandas/_libs/period.pyx @@ -14,7 +14,6 @@ from libc.stdlib cimport free from pandas import compat from pandas.compat import PY2 -from pandas.core.dtypes.generic import ABCDateOffset cimport cython @@ -44,7 +43,7 @@ from tslibs.timezones cimport ( maybe_get_tz) from pandas.core.tools.datetimes import parse_time_string -from pandas.tseries import frequencies +from pandas.tseries import frequencies, offsets cdef int64_t NPY_NAT = util.get_nat() @@ -119,11 +118,6 @@ cdef extern from "period_helper.h": initialize_daytime_conversion_factor_matrix() -cpdef _is_tick(item): - # offsets.Tick subclasses offsets.DateOffset and has a "_inc" attribute - return isinstance(item, ABCDateOffset) and hasattr(item, "_inc") - - # Period logic #---------------------------------------------------------------------- @@ -755,9 +749,9 @@ cdef class _Period(object): return hash((self.ordinal, self.freqstr)) def _add_delta(self, other): - if isinstance(other, (timedelta, np.timedelta64)) or _is_tick(other): + if isinstance(other, (timedelta, np.timedelta64, offsets.Tick)): offset = frequencies.to_offset(self.freq.rule_code) - if _is_tick(offset): + if isinstance(offset, offsets.Tick): nanos = tslib._delta_to_nanoseconds(other) offset_nanos = tslib._delta_to_nanoseconds(offset) @@ -766,7 +760,7 @@ cdef class _Period(object): return Period(ordinal=ordinal, freq=self.freq) msg = 'Input cannot be converted to Period(freq={0})' raise IncompatibleFrequency(msg.format(self.freqstr)) - elif isinstance(other, ABCDateOffset): + elif isinstance(other, offsets.DateOffset): freqstr = other.rule_code base = frequencies.get_base_alias(freqstr) if base == self.freq.rule_code: @@ -779,7 +773,8 @@ cdef class _Period(object): def __add__(self, other): if isinstance(self, Period): - if isinstance(other, (timedelta, np.timedelta64, ABCDateOffset)): + if isinstance(other, + (timedelta, np.timedelta64, offsets.DateOffset)): return self._add_delta(other) elif other is NaT: return NaT @@ -795,7 +790,8 @@ cdef class _Period(object): def __sub__(self, other): if isinstance(self, Period): - if isinstance(other, (timedelta, np.timedelta64, ABCDateOffset)): + if isinstance(other, + (timedelta, np.timedelta64, offsets.DateOffset)): neg_other = -other return self + neg_other elif lib.is_integer(other): From 13e481d7354036cc1eb0aeaec6ea719d8cf26f52 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 23 Aug 2017 12:22:11 -0700 Subject: [PATCH 5/9] Add period_helper to period sources Wild guess that it might be causing the build fail on appveyor --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index afb25ad5ec565..9ef92bb178eb9 100755 --- a/setup.py +++ b/setup.py @@ -493,7 +493,8 @@ def pxd(name): 'depends': tseries_depends + \ ['pandas/_libs/tslibs/timezones.pyx'], 'sources': ['pandas/_libs/src/datetime/np_datetime.c', - 'pandas/_libs/src/datetime/np_datetime_strings.c']}, + 'pandas/_libs/src/datetime/np_datetime_strings.c', + 'pandas/_libs/src/period_helper.c']}, '_libs.index': {'pyxfile': '_libs/index', 'sources': ['pandas/_libs/src/datetime/np_datetime.c', 'pandas/_libs/src/datetime/np_datetime_strings.c'], From 1a622988a8e56d6a691025ca87aa484d16d90136 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 24 Aug 2017 09:05:39 -0700 Subject: [PATCH 6/9] Address reviewer comments; revert non-central fixes --- pandas/_libs/period.pyx | 24 +++++++++++++++--------- pandas/_libs/tslib.pyx | 2 +- pandas/_libs/tslibs/timezones.pyx | 25 +++++-------------------- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/pandas/_libs/period.pyx b/pandas/_libs/period.pyx index 8bf81597f6b1d..0def28c9d92b9 100644 --- a/pandas/_libs/period.pyx +++ b/pandas/_libs/period.pyx @@ -10,6 +10,9 @@ from numpy cimport (int8_t, int32_t, int64_t, import_array, ndarray, NPY_INT64, NPY_DATETIME, NPY_TIMEDELTA) import numpy as np +cdef extern from "datetime_helper.h": + double total_seconds(object) + from libc.stdlib cimport free from pandas import compat @@ -17,11 +20,9 @@ from pandas.compat import PY2 cimport cython -cdef extern from "datetime.h": - void PyDateTime_IMPORT() - from datetime cimport ( is_leapyear, + PyDateTime_IMPORT pandas_datetimestruct, pandas_datetimestruct_to_datetime, pandas_datetime_to_datetimestruct, @@ -31,19 +32,19 @@ from datetime cimport ( cimport util, lib from lib cimport is_null_datetimelike, is_period from pandas._libs import tslib, lib -from pandas._libs.tslib import Timestamp, iNaT, NaT +from pandas._libs.tslib import Timedelta, Timestamp, iNaT, NaT from tslib cimport _nat_scalar_rules from tslibs.timezones cimport ( - total_seconds, _get_utcoffset, _get_dst_info, _is_tzlocal, _is_utc, maybe_get_tz) +from pandas.tseries import offsets from pandas.core.tools.datetimes import parse_time_string -from pandas.tseries import frequencies, offsets +from pandas.tseries import frequencies cdef int64_t NPY_NAT = util.get_nat() @@ -727,7 +728,8 @@ cdef class _Period(object): return hash((self.ordinal, self.freqstr)) def _add_delta(self, other): - if isinstance(other, (timedelta, np.timedelta64, offsets.Tick)): + if isinstance(other, (timedelta, np.timedelta64, + offsets.Tick, Timedelta)): offset = frequencies.to_offset(self.freq.rule_code) if isinstance(offset, offsets.Tick): nanos = tslib._delta_to_nanoseconds(other) @@ -752,7 +754,9 @@ cdef class _Period(object): def __add__(self, other): if isinstance(self, Period): if isinstance(other, - (timedelta, np.timedelta64, offsets.DateOffset)): + (timedelta, np.timedelta64, + offsets.Tick, offsets.DateOffset, + Timedelta)): return self._add_delta(other) elif other is NaT: return NaT @@ -769,7 +773,9 @@ cdef class _Period(object): def __sub__(self, other): if isinstance(self, Period): if isinstance(other, - (timedelta, np.timedelta64, offsets.DateOffset)): + (timedelta, np.timedelta64, + offsets.Tick, offsets.DateOffset, + Timedelta)): neg_other = -other return self + neg_other elif lib.is_integer(other): diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index 43db3dcf82535..7fbed9feaa436 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -3884,7 +3884,7 @@ for _maybe_method_name in dir(NaTType): def f(*args, **kwargs): raise ValueError("NaTType does not support " + func_name) f.__name__ = func_name - f.__doc__ = _get_docstring(func_name) + f.__doc__ = _get_docstring(_method_name) return f setattr(NaTType, _maybe_method_name, diff --git a/pandas/_libs/tslibs/timezones.pyx b/pandas/_libs/tslibs/timezones.pyx index b08b2de471be5..4d78e83e31101 100644 --- a/pandas/_libs/tslibs/timezones.pyx +++ b/pandas/_libs/tslibs/timezones.pyx @@ -2,8 +2,7 @@ # -*- coding: utf-8 -*- # cython: profile=False -try: string_types = basestring -except NameError: string_types = str +from pandas.compat import string_types, is_platform_windows cdef extern from "Python.h": @@ -29,9 +28,7 @@ from dateutil.tz import (tzoffset, tzutc as _dateutil_tzutc, tzstr as _dateutil_tzstr) -import sys -if sys.platform == 'win32' or sys.platform == 'cygwin': - # equiv pd.compat.is_platform_windows() +if is_platform_windows(): from dateutil.zoneinfo import gettz as _dateutil_gettz else: from dateutil.tz import gettz as _dateutil_gettz @@ -55,18 +52,6 @@ cdef inline bint is_integer_object(object obj): cdef int64_t NPY_NAT = np.datetime64('nat').astype(np.int64) -# TODO: Does this belong somewhere else? -cdef float64_t total_seconds(object td): - # Note: This is marginally faster than the version in datetime_helper. - cdef int64_t microseconds, seconds, days, days_in_seconds - - microseconds = getattr(td, "microseconds", 0) - seconds = getattr(td, "seconds", 0) - days = getattr(td, "days", 0) - days_in_seconds = days * 24 * 3600; - return (microseconds + (seconds + days_in_seconds) * 1000000.0) / 1000000.0 - - #---------------------------------------------------------------------- # time zone conversion helpers @@ -208,7 +193,7 @@ cpdef ndarray _unbox_utcoffsets(object transinfo): arr = np.empty(sz, dtype='i8') for i in range(sz): - arr[i] = int(total_seconds(transinfo[i][0])) * 1000000000 + arr[i] = int(transinfo[i][0].total_seconds()) * 1000000000 return arr @@ -260,7 +245,7 @@ cpdef object _get_dst_info(object tz): """ cache_key = _tz_cache_key(tz) if cache_key is None: - num = int(total_seconds(_get_utcoffset(tz, None))) * 1000000000 + num = int(_get_utcoffset(tz, None).total_seconds()) * 1000000000 return (np.array([NPY_NAT + 1], dtype=np.int64), np.array([num], dtype=np.int64), None) @@ -307,7 +292,7 @@ cpdef object _get_dst_info(object tz): else: # static tzinfo trans = np.array([NPY_NAT + 1], dtype=np.int64) - num = int(total_seconds(_get_utcoffset(tz, None))) * 1000000000 + num = int(_get_utcoffset(tz, None).total_seconds()) * 1000000000 deltas = np.array([num], dtype=np.int64) typ = 'static' From 0c1ba132be0a10b997628d772de4e4e06096de52 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 24 Aug 2017 15:28:03 -0700 Subject: [PATCH 7/9] missing comma --- pandas/_libs/period.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/period.pyx b/pandas/_libs/period.pyx index 0def28c9d92b9..9421c53e7993e 100644 --- a/pandas/_libs/period.pyx +++ b/pandas/_libs/period.pyx @@ -22,7 +22,7 @@ cimport cython from datetime cimport ( is_leapyear, - PyDateTime_IMPORT + PyDateTime_IMPORT, pandas_datetimestruct, pandas_datetimestruct_to_datetime, pandas_datetime_to_datetimestruct, From f29e6e590cd734a60d45e8a8e32b52e0bcb0e4b8 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 24 Aug 2017 19:46:46 -0700 Subject: [PATCH 8/9] Remove total_seconds from pxd --- pandas/_libs/tslibs/timezones.pxd | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/_libs/tslibs/timezones.pxd b/pandas/_libs/tslibs/timezones.pxd index 0a39454fd794b..59efb24600966 100644 --- a/pandas/_libs/tslibs/timezones.pxd +++ b/pandas/_libs/tslibs/timezones.pxd @@ -18,5 +18,3 @@ cdef bint _is_fixed_offset(object tz) cdef object _get_utc_trans_times_from_dateutil_tz(object tz) cpdef object _get_dst_info(object tz) - -cdef float64_t total_seconds(object td) From 0540e7733828bc0a587fa23c87ead30d285c16d1 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 28 Aug 2017 10:29:27 -0700 Subject: [PATCH 9/9] dummy commit to force CI --- pandas/_libs/tslib.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index c9904fbe37eb4..1f72ccbee6568 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # cython: profile=False import warnings