Repository: pandas-dev/pandas

Issue:
localize_tso unnecessary calls?
Related: #17876, #17734 

In `tslib._localize_tso` there are 4 of blocks of the form:

```
        if obj.value != NPY_NAT:
            pandas_datetime_to_datetimestruct(obj.value + delta,
                                              PANDAS_FR_ns, &obj.dts)
        else:
            pandas_datetime_to_datetimestruct(obj.value,
                                              PANDAS_FR_ns, &obj.dts)

```

But in the case where `obj.value == NPY_NAT`, can't we skip the call to `pandas_datetime_to_datetimestruct` because we're going to end up with `NaT` anyway?


Discussion:
@jbrockmendel : There's one way to find out: remove the calls and run the tests :smile: 
Doesn't cause any test failures; kind of hoping for outside confirmation though.
This can be fixed in a follow-up after #17875 is closed


Resolving patch:
diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py
--- a/pandas/core/indexes/datetimes.py
+++ b/pandas/core/indexes/datetimes.py
@@ -55,8 +55,7 @@
 from pandas._libs import (lib, index as libindex, tslib as libts,
                           algos as libalgos, join as libjoin,
                           Timestamp, period as libperiod)
-from pandas._libs.tslibs import timezones
-
+from pandas._libs.tslibs import timezones, conversion
 
 # -------- some conversion wrapper functions
 
@@ -384,8 +383,8 @@ def __new__(cls, data=None,
                         getattr(data, 'tz', None) is None):
                     # Convert tz-naive to UTC
                     ints = subarr.view('i8')
-                    subarr = libts.tz_localize_to_utc(ints, tz,
-                                                      ambiguous=ambiguous)
+                    subarr = conversion.tz_localize_to_utc(ints, tz,
+                                                           ambiguous=ambiguous)
                 subarr = subarr.view(_NS_DTYPE)
 
         subarr = cls._simple_new(subarr, name=name, freq=freq, tz=tz)
@@ -531,8 +530,8 @@ def _generate(cls, start, end, periods, name, offset,
                 index = _generate_regular_range(start, end, periods, offset)
 
             if tz is not None and getattr(index, 'tz', None) is None:
-                index = libts.tz_localize_to_utc(_ensure_int64(index), tz,
-                                                 ambiguous=ambiguous)
+                index = conversion.tz_localize_to_utc(_ensure_int64(index), tz,
+                                                      ambiguous=ambiguous)
                 index = index.view(_NS_DTYPE)
 
                 # index is localized datetime64 array -> have to convert
@@ -561,11 +560,11 @@ def _convert_for_op(self, value):
 
     def _local_timestamps(self):
         if self.is_monotonic:
-            return libts.tz_convert(self.asi8, utc, self.tz)
+            return conversion.tz_convert(self.asi8, utc, self.tz)
         else:
             values = self.asi8
             indexer = values.argsort()
-            result = libts.tz_convert(values.take(indexer), utc, self.tz)
+            result = conversion.tz_convert(values.take(indexer), utc, self.tz)
 
             n = len(indexer)
             reverse = np.empty(n, dtype=np.int_)
@@ -1644,7 +1643,7 @@ def normalize(self):
         -------
         normalized : DatetimeIndex
         """
-        new_values = libts.date_normalize(self.asi8, self.tz)
+        new_values = conversion.date_normalize(self.asi8, self.tz)
         return DatetimeIndex(new_values, freq='infer', name=self.name,
                              tz=self.tz)
 
@@ -1683,7 +1682,7 @@ def is_normalized(self):
         """
         Returns True if all of the dates are at midnight ("no time")
         """
-        return libts.dates_normalized(self.asi8, self.tz)
+        return conversion.is_date_array_normalized(self.asi8, self.tz)
 
     @cache_readonly
     def _resolution(self):
@@ -1724,7 +1723,7 @@ def insert(self, loc, item):
             new_dates = np.concatenate((self[:loc].asi8, [item.view(np.int64)],
                                         self[loc:].asi8))
             if self.tz is not None:
-                new_dates = libts.tz_convert(new_dates, 'UTC', self.tz)
+                new_dates = conversion.tz_convert(new_dates, 'UTC', self.tz)
             return DatetimeIndex(new_dates, name=self.name, freq=freq,
                                  tz=self.tz)
 
@@ -1764,7 +1763,7 @@ def delete(self, loc):
                     freq = self.freq
 
         if self.tz is not None:
-            new_dates = libts.tz_convert(new_dates, 'UTC', self.tz)
+            new_dates = conversion.tz_convert(new_dates, 'UTC', self.tz)
         return DatetimeIndex(new_dates, name=self.name, freq=freq, tz=self.tz)
 
     def tz_convert(self, tz):
@@ -1844,16 +1843,16 @@ def tz_localize(self, tz, ambiguous='raise', errors='raise'):
         """
         if self.tz is not None:
             if tz is None:
-                new_dates = libts.tz_convert(self.asi8, 'UTC', self.tz)
+                new_dates = conversion.tz_convert(self.asi8, 'UTC', self.tz)
             else:
                 raise TypeError("Already tz-aware, use tz_convert to convert.")
         else:
             tz = timezones.maybe_get_tz(tz)
             # Convert to UTC
 
-            new_dates = libts.tz_localize_to_utc(self.asi8, tz,
-                                                 ambiguous=ambiguous,
-                                                 errors=errors)
+            new_dates = conversion.tz_localize_to_utc(self.asi8, tz,
+                                                      ambiguous=ambiguous,
+                                                      errors=errors)
         new_dates = new_dates.view(_NS_DTYPE)
         return self._shallow_copy(new_dates, tz=tz)
 
@@ -2194,7 +2193,7 @@ def _to_m8(key, tz=None):
         # this also converts strings
         key = Timestamp(key, tz=tz)
 
-    return np.int64(libts.pydt_to_i8(key)).view(_NS_DTYPE)
+    return np.int64(conversion.pydt_to_i8(key)).view(_NS_DTYPE)
 
 
 _CACHE_START = Timestamp(datetime(1950, 1, 1))
diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py
--- a/pandas/tseries/frequencies.py
+++ b/pandas/tseries/frequencies.py
@@ -21,6 +21,7 @@
 
 from pandas._libs import lib, tslib
 from pandas._libs.tslib import Timedelta
+from pandas._libs.tslibs import conversion
 from pandas._libs.tslibs.frequencies import (  # noqa
     get_freq_code, _base_and_stride, _period_str_to_code,
     _INVALID_FREQ_ERROR, opattern, _lite_rule_alias, _dont_uppercase,
@@ -583,7 +584,8 @@ def __init__(self, index, warn=True):
         # the timezone so they are in local time
         if hasattr(index, 'tz'):
             if index.tz is not None:
-                self.values = tslib.tz_convert(self.values, 'UTC', index.tz)
+                self.values = conversion.tz_convert(self.values,
+                                                    'UTC', index.tz)
 
         self.warn = warn
 


Test patch:
diff --git a/pandas/tests/scalar/test_timestamp.py b/pandas/tests/scalar/test_timestamp.py
--- a/pandas/tests/scalar/test_timestamp.py
+++ b/pandas/tests/scalar/test_timestamp.py
@@ -16,8 +16,9 @@
 
 import pandas.util.testing as tm
 from pandas.tseries import offsets, frequencies
-from pandas._libs import tslib, period
+from pandas._libs import period
 from pandas._libs.tslibs.timezones import get_timezone
+from pandas._libs.tslibs import conversion
 
 from pandas.compat import lrange, long, PY3
 from pandas.util.testing import assert_series_equal
@@ -77,12 +78,12 @@ def test_constructor(self):
             for result in [Timestamp(date_str), Timestamp(date)]:
                 # only with timestring
                 assert result.value == expected
-                assert tslib.pydt_to_i8(result) == expected
+                assert conversion.pydt_to_i8(result) == expected
 
                 # re-creation shouldn't affect to internal value
                 result = Timestamp(result)
                 assert result.value == expected
-                assert tslib.pydt_to_i8(result) == expected
+                assert conversion.pydt_to_i8(result) == expected
 
             # with timezone
             for tz, offset in timezones:
@@ -90,18 +91,18 @@ def test_constructor(self):
                                                                      tz=tz)]:
                     expected_tz = expected - offset * 3600 * 1000000000
                     assert result.value == expected_tz
-                    assert tslib.pydt_to_i8(result) == expected_tz
+                    assert conversion.pydt_to_i8(result) == expected_tz
 
                     # should preserve tz
                     result = Timestamp(result)
                     assert result.value == expected_tz
-                    assert tslib.pydt_to_i8(result) == expected_tz
+                    assert conversion.pydt_to_i8(result) == expected_tz
 
                     # should convert to UTC
                     result = Timestamp(result, tz='UTC')
                     expected_utc = expected - offset * 3600 * 1000000000
                     assert result.value == expected_utc
-                    assert tslib.pydt_to_i8(result) == expected_utc
+                    assert conversion.pydt_to_i8(result) == expected_utc
 
     def test_constructor_with_stringoffset(self):
         # GH 7833
@@ -129,30 +130,30 @@ def test_constructor_with_stringoffset(self):
             for result in [Timestamp(date_str)]:
                 # only with timestring
                 assert result.value == expected
-                assert tslib.pydt_to_i8(result) == expected
+                assert conversion.pydt_to_i8(result) == expected
 
                 # re-creation shouldn't affect to internal value
                 result = Timestamp(result)
                 assert result.value == expected
-                assert tslib.pydt_to_i8(result) == expected
+                assert conversion.pydt_to_i8(result) == expected
 
             # with timezone
             for tz, offset in timezones:
                 result = Timestamp(date_str, tz=tz)
                 expected_tz = expected
                 assert result.value == expected_tz
-                assert tslib.pydt_to_i8(result) == expected_tz
+                assert conversion.pydt_to_i8(result) == expected_tz
 
                 # should preserve tz
                 result = Timestamp(result)
                 assert result.value == expected_tz
-                assert tslib.pydt_to_i8(result) == expected_tz
+                assert conversion.pydt_to_i8(result) == expected_tz
 
                 # should convert to UTC
                 result = Timestamp(result, tz='UTC')
                 expected_utc = expected
                 assert result.value == expected_utc
-                assert tslib.pydt_to_i8(result) == expected_utc
+                assert conversion.pydt_to_i8(result) == expected_utc
 
         # This should be 2013-11-01 05:00 in UTC
         # converted to Chicago tz
diff --git a/pandas/tests/tseries/test_timezones.py b/pandas/tests/tseries/test_timezones.py
--- a/pandas/tests/tseries/test_timezones.py
+++ b/pandas/tests/tseries/test_timezones.py
@@ -17,7 +17,7 @@
 from pandas.core.indexes.datetimes import bdate_range, date_range
 from pandas.core.dtypes.dtypes import DatetimeTZDtype
 from pandas._libs import tslib
-from pandas._libs.tslibs import timezones
+from pandas._libs.tslibs import timezones, conversion
 from pandas import (Index, Series, DataFrame, isna, Timestamp, NaT,
                     DatetimeIndex, to_datetime)
 from pandas.util.testing import (assert_frame_equal, assert_series_equal,
@@ -1738,14 +1738,14 @@ class TestTslib(object):
 
     def test_tslib_tz_convert(self):
         def compare_utc_to_local(tz_didx, utc_didx):
-            f = lambda x: tslib.tz_convert_single(x, 'UTC', tz_didx.tz)
-            result = tslib.tz_convert(tz_didx.asi8, 'UTC', tz_didx.tz)
+            f = lambda x: conversion.tz_convert_single(x, 'UTC', tz_didx.tz)
+            result = conversion.tz_convert(tz_didx.asi8, 'UTC', tz_didx.tz)
             result_single = np.vectorize(f)(tz_didx.asi8)
             tm.assert_numpy_array_equal(result, result_single)
 
         def compare_local_to_utc(tz_didx, utc_didx):
-            f = lambda x: tslib.tz_convert_single(x, tz_didx.tz, 'UTC')
-            result = tslib.tz_convert(utc_didx.asi8, tz_didx.tz, 'UTC')
+            f = lambda x: conversion.tz_convert_single(x, tz_didx.tz, 'UTC')
+            result = conversion.tz_convert(utc_didx.asi8, tz_didx.tz, 'UTC')
             result_single = np.vectorize(f)(utc_didx.asi8)
             tm.assert_numpy_array_equal(result, result_single)
 
@@ -1770,14 +1770,14 @@ def compare_local_to_utc(tz_didx, utc_didx):
             compare_local_to_utc(tz_didx, utc_didx)
 
         # Check empty array
-        result = tslib.tz_convert(np.array([], dtype=np.int64),
-                                  timezones.maybe_get_tz('US/Eastern'),
-                                  timezones.maybe_get_tz('Asia/Tokyo'))
+        result = conversion.tz_convert(np.array([], dtype=np.int64),
+                                       timezones.maybe_get_tz('US/Eastern'),
+                                       timezones.maybe_get_tz('Asia/Tokyo'))
         tm.assert_numpy_array_equal(result, np.array([], dtype=np.int64))
 
         # Check all-NaT array
-        result = tslib.tz_convert(np.array([tslib.iNaT], dtype=np.int64),
-                                  timezones.maybe_get_tz('US/Eastern'),
-                                  timezones.maybe_get_tz('Asia/Tokyo'))
+        result = conversion.tz_convert(np.array([tslib.iNaT], dtype=np.int64),
+                                       timezones.maybe_get_tz('US/Eastern'),
+                                       timezones.maybe_get_tz('Asia/Tokyo'))
         tm.assert_numpy_array_equal(result, np.array(
             [tslib.iNaT], dtype=np.int64))