Skip to content

BUG: Fix Timestamp type checks to work with subclassed datetime (#25851) #25853

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Apr 5, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2e5a160
BUG: Fix Timestamp type checks to work with subclassed datetime (#25851)
ArtificialQualia Mar 23, 2019
82b2420
increase spacing in comment
ArtificialQualia Mar 23, 2019
4447746
fix spacing
ArtificialQualia Mar 23, 2019
2d20375
fix import formatting
ArtificialQualia Mar 24, 2019
8e8d816
move _Timestamp to separate file to prevent circular import
ArtificialQualia Mar 28, 2019
4fd35ac
fix indenting
ArtificialQualia Mar 28, 2019
354d3a3
fix line spacing
ArtificialQualia Mar 28, 2019
600be4f
kickoff new build
ArtificialQualia Mar 28, 2019
7a60942
updates for PR review
ArtificialQualia Mar 29, 2019
dd5d492
Merge branch 'master' into update_timestamp_checks
ArtificialQualia Mar 29, 2019
46f306d
fix isort error
ArtificialQualia Mar 29, 2019
7268a91
Merge branch 'update_timestamp_checks' of https://github.com/Artifici…
ArtificialQualia Mar 29, 2019
ff0ddef
fix isort error
ArtificialQualia Mar 29, 2019
c54ee53
force rebuild
ArtificialQualia Mar 30, 2019
6dc805b
PR updates
ArtificialQualia Mar 30, 2019
789e121
Merge branch 'master' into update_timestamp_checks
ArtificialQualia Mar 30, 2019
6a66238
parameterize test
ArtificialQualia Mar 30, 2019
12a53be
Merge branch 'update_timestamp_checks' of https://github.com/Artifici…
ArtificialQualia Mar 30, 2019
7da7684
PR updates
ArtificialQualia Mar 31, 2019
dc0f806
fix namespace test for new api
ArtificialQualia Mar 31, 2019
0cad612
fix typo
ArtificialQualia Mar 31, 2019
a1cb913
Merge branch 'master' into update_timestamp_checks
ArtificialQualia Apr 5, 2019
ee9d2d5
merge changes into _timestamp
ArtificialQualia Apr 5, 2019
560ef6f
change _timestamp files to c_timestamp
ArtificialQualia Apr 5, 2019
92a9c52
Merge branch 'master' into update_timestamp_checks
ArtificialQualia Apr 5, 2019
b87aa99
use new tzconversion.tz_convert_single merged function
ArtificialQualia Apr 5, 2019
11a091a
move imports to fix import exceptions
ArtificialQualia Apr 5, 2019
4896568
change imports
ArtificialQualia Apr 5, 2019
43ec600
fix api test
ArtificialQualia Apr 5, 2019
9795807
force rebuild
ArtificialQualia Apr 5, 2019
ded5e69
force rebuild
ArtificialQualia Apr 5, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v0.25.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ Sparse
Other
^^^^^

-
- Improved :class:`Timestamp` type checking in various datetime functions to prevent exceptions when using a subclassed `datetime` (:issue:`25851`)
-
-

Expand Down
7 changes: 3 additions & 4 deletions pandas/_libs/tslib.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import cython

from cpython.datetime cimport (PyDateTime_Check, PyDate_Check,
PyDateTime_CheckExact,
PyDateTime_IMPORT,
timedelta, datetime, date, time)
# import datetime C API
Expand Down Expand Up @@ -41,7 +40,8 @@ from pandas._libs.tslibs.nattype cimport (

from pandas._libs.tslibs.offsets cimport to_offset

from pandas._libs.tslibs.timestamps cimport create_timestamp_from_ts
from pandas._libs.tslibs.timestamps cimport (
create_timestamp_from_ts, _Timestamp)
from pandas._libs.tslibs.timestamps import Timestamp


Expand Down Expand Up @@ -539,8 +539,7 @@ cpdef array_to_datetime(ndarray[object] values, str errors='raise',
'datetime64 unless utc=True')
else:
iresult[i] = pydatetime_to_dt64(val, &dts)
if not PyDateTime_CheckExact(val):
# i.e. a Timestamp object
if isinstance(val, _Timestamp):
iresult[i] += val.nanosecond
check_dts_bounds(&dts)

Expand Down
2 changes: 1 addition & 1 deletion pandas/_libs/tslibs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# flake8: noqa

from .timestamps import Timestamp # isort:skip
from .conversion import normalize_date, localize_pydatetime, tz_convert_single
from .nattype import NaT, NaTType, iNaT, is_null_datetimelike
from .np_datetime import OutOfBoundsDatetime
from .period import Period, IncompatibleFrequency
from .timestamps import Timestamp
from .timedeltas import delta_to_nanoseconds, ints_to_pytimedelta, Timedelta
13 changes: 5 additions & 8 deletions pandas/_libs/tslibs/conversion.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ from dateutil.tz import tzutc
from datetime import time as datetime_time
from cpython.datetime cimport (datetime, tzinfo,
PyDateTime_Check, PyDate_Check,
PyDateTime_CheckExact, PyDateTime_IMPORT,
PyDelta_Check)
PyDateTime_IMPORT, PyDelta_Check)
PyDateTime_IMPORT

from pandas._libs.tslibs.ccalendar import DAY_SECONDS, HOUR_SECONDS
Expand All @@ -31,6 +30,7 @@ from pandas._libs.tslibs.util cimport (

from pandas._libs.tslibs.timedeltas cimport (cast_from_unit,
delta_to_nanoseconds)
from pandas._libs.tslibs.timestamps cimport _Timestamp
from pandas._libs.tslibs.timezones cimport (
is_utc, is_tzlocal, is_fixed_offset, get_utcoffset, get_dst_info,
get_timezone, maybe_get_tz, tz_compare)
Expand Down Expand Up @@ -379,8 +379,7 @@ cdef _TSObject convert_datetime_to_tsobject(datetime ts, object tz,
offset = get_utcoffset(obj.tzinfo, ts)
obj.value -= int(offset.total_seconds() * 1e9)

if not PyDateTime_CheckExact(ts):
# datetime instance but not datetime type --> Timestamp
if isinstance(ts, _Timestamp):
obj.value += ts.nanosecond
obj.dts.ps = ts.nanosecond * 1000

Expand Down Expand Up @@ -607,8 +606,7 @@ cpdef inline datetime localize_pydatetime(datetime dt, object tz):
"""
if tz is None:
return dt
elif not PyDateTime_CheckExact(dt):
# i.e. is a Timestamp
elif isinstance(dt, _Timestamp):
return dt.tz_localize(tz)
elif is_utc(tz):
return _localize_pydatetime(dt, tz)
Expand Down Expand Up @@ -1155,8 +1153,7 @@ def normalize_date(dt: object) -> datetime:
TypeError : if input is not datetime.date, datetime.datetime, or Timestamp
"""
if PyDateTime_Check(dt):
if not PyDateTime_CheckExact(dt):
# i.e. a Timestamp object
if isinstance(dt, _Timestamp):
return dt.replace(hour=0, minute=0, second=0, microsecond=0,
nanosecond=0)
else:
Expand Down
9 changes: 5 additions & 4 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ from numpy cimport int64_t
cnp.import_array()

from cpython.datetime cimport (datetime, timedelta,
PyDateTime_CheckExact,
PyDateTime_Check, PyDelta_Check,
PyDateTime_IMPORT)
PyDateTime_IMPORT
Expand All @@ -37,6 +36,7 @@ from pandas._libs.tslibs.nattype cimport (
checknull_with_nat, NPY_NAT, c_NaT as NaT)
from pandas._libs.tslibs.offsets cimport to_offset
from pandas._libs.tslibs.offsets import _Tick as Tick
from pandas._libs.tslibs.timestamps cimport _Timestamp

# ----------------------------------------------------------------------
# Constants
Expand Down Expand Up @@ -583,9 +583,10 @@ def _binary_op_method_timedeltalike(op, name):
# has-dtype check before then
pass

elif is_datetime64_object(other) or PyDateTime_CheckExact(other):
# the PyDateTime_CheckExact case is for a datetime object that
# is specifically *not* a Timestamp, as the Timestamp case will be
elif is_datetime64_object(other) or (
PyDateTime_Check(other) and not isinstance(other, _Timestamp)):
# this case is for a datetime object that is specifically
# *not* a Timestamp, as the Timestamp case will be
# handled after `_validate_ops_compat` returns False below
from pandas._libs.tslibs.timestamps import Timestamp
return op(self, Timestamp(other))
Expand Down
16 changes: 16 additions & 0 deletions pandas/_libs/tslibs/timestamps.pxd
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
# -*- coding: utf-8 -*-

from cpython.datetime cimport datetime

from numpy cimport int64_t
from pandas._libs.tslibs.np_datetime cimport npy_datetimestruct

cdef object create_timestamp_from_ts(int64_t value,
npy_datetimestruct dts,
object tz, object freq)

cdef class _Timestamp(datetime):
cdef readonly:
int64_t value, nanosecond
object freq
list _date_attributes
cpdef bint _get_start_end_field(self, str field)
cpdef _get_date_name_field(self, object field, object locale)
cdef int64_t _maybe_convert_value_to_local(self)
cpdef to_datetime64(self)
cdef _assert_tzawareness_compat(_Timestamp self, datetime other)
cpdef datetime to_pydatetime(_Timestamp self, bint warn=*)
cdef bint _compare_outside_nanorange(_Timestamp self, datetime other,
int op) except -1
9 changes: 2 additions & 7 deletions pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ from pandas._libs.tslibs.util cimport (

cimport pandas._libs.tslibs.ccalendar as ccalendar
from pandas._libs.tslibs.ccalendar import DAY_SECONDS
from pandas._libs.tslibs.conversion import (
tz_localize_to_utc, normalize_i8_timestamps)
from pandas._libs.tslibs.conversion cimport (
tz_convert_single, _TSObject, convert_to_tsobject,
convert_datetime_to_tsobject)
Expand Down Expand Up @@ -203,11 +201,6 @@ def round_nsint64(values, mode, freq):
# shadows the python class, where we do any heavy lifting.
cdef class _Timestamp(datetime):

cdef readonly:
int64_t value, nanosecond
object freq # frequency reference
list _date_attributes

def __hash__(_Timestamp self):
if self.nanosecond:
return hash(self.value)
Expand Down Expand Up @@ -1215,6 +1208,7 @@ class Timestamp(_Timestamp):
tz = maybe_get_tz(tz)
if not is_string_object(ambiguous):
ambiguous = [ambiguous]
from pandas._libs.tslibs.conversion import tz_localize_to_utc
value = tz_localize_to_utc(np.array([self.value], dtype='i8'), tz,
ambiguous=ambiguous,
nonexistent=nonexistent)[0]
Expand Down Expand Up @@ -1409,6 +1403,7 @@ class Timestamp(_Timestamp):
DAY_NS = DAY_SECONDS * 1000000000
normalized_value = self.value - (self.value % DAY_NS)
return Timestamp(normalized_value).tz_localize(self.tz)
from pandas._libs.tslibs.conversion import normalize_i8_timestamps
normalized_value = normalize_i8_timestamps(
np.array([self.value], dtype='i8'), tz=self.tz)[0]
return Timestamp(normalized_value).tz_localize(self.tz)
Expand Down
11 changes: 11 additions & 0 deletions pandas/tests/arithmetic/test_datetime64.py
Original file line number Diff line number Diff line change
Expand Up @@ -2351,3 +2351,14 @@ def test_shift_months(years, months):
for x in dti]
expected = DatetimeIndex(raw)
tm.assert_index_equal(actual, expected)


def test_dt_subclass_add_timedelta():
# GH 25851
class SubDatetime(datetime):
pass
dt = SubDatetime(2000, 1, 1)
td = Timedelta(hours=1)
result = dt + td
expected = SubDatetime(2000, 1, 1, 1)
assert result == expected
9 changes: 9 additions & 0 deletions pandas/tests/scalar/timestamp/test_timestamp.py
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,15 @@ def test_dont_convert_dateutil_utc_to_pytz_utc(self):
expected = Timestamp(datetime(2018, 1, 1)).tz_localize(tzutc())
assert result == expected

def test_constructor_subclassed_datetime(self):
# GH 25851
class SubDatetime(datetime):
pass
data = SubDatetime(2000, 1, 1)
result = Timestamp(data)
expected = Timestamp(2000, 1, 1)
assert result == expected


class TestTimestamp(object):

Expand Down
23 changes: 23 additions & 0 deletions pandas/tests/tslibs/test_array_to_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pandas._libs import iNaT, tslib
from pandas.compat.numpy import np_array_datetime64_compat

from pandas import Timestamp
import pandas.util.testing as tm


Expand Down Expand Up @@ -154,3 +155,25 @@ def test_to_datetime_barely_out_of_bounds():

with pytest.raises(tslib.OutOfBoundsDatetime, match=msg):
tslib.array_to_datetime(arr)


class SubDatetime(datetime):
pass


@pytest.mark.parametrize("data,expected", [
([SubDatetime(2000, 1, 1)],
["2000-01-01T00:00:00.000000000-0000"]),
([datetime(2000, 1, 1)],
["2000-01-01T00:00:00.000000000-0000"]),
([Timestamp(2000, 1, 1)],
["2000-01-01T00:00:00.000000000-0000"])
])
def test_datetime_subclass(data, expected):
# GH 25851

arr = np.array(data, dtype=object)
result, _ = tslib.array_to_datetime(arr)

expected = np_array_datetime64_compat(expected, dtype="M8[ns]")
tm.assert_numpy_array_equal(result, expected)
23 changes: 22 additions & 1 deletion pandas/tests/tslibs/test_conversion.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-

from datetime import datetime

import numpy as np
import pytest
from pytz import UTC

from pandas._libs.tslib import iNaT
from pandas._libs.tslibs import conversion, timezones

from pandas import date_range
from pandas import Timestamp, date_range
import pandas.util.testing as tm


Expand Down Expand Up @@ -66,3 +68,22 @@ def test_length_zero_copy(dtype, copy):
arr = np.array([], dtype=dtype)
result = conversion.ensure_datetime64ns(arr, copy=copy)
assert result.base is (None if copy else arr)


class SubDatetime(datetime):
pass


@pytest.mark.parametrize("dt, expected", [
pytest.param(Timestamp("2000-01-01"),
Timestamp("2000-01-01", tz=UTC), id="timestamp"),
pytest.param(datetime(2000, 1, 1),
datetime(2000, 1, 1, tzinfo=UTC),
id="datetime"),
pytest.param(SubDatetime(2000, 1, 1),
SubDatetime(2000, 1, 1, tzinfo=UTC),
id="subdatetime")])
def test_localize_pydatetime_dt_types(dt, expected):
# GH 25851
result = conversion.localize_pydatetime(dt, UTC)
assert result == expected
19 changes: 19 additions & 0 deletions pandas/tests/tslibs/test_normalize_date.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

from pandas._libs import tslibs

from pandas import Timestamp


@pytest.mark.parametrize("value,expected", [
(date(2012, 9, 7), datetime(2012, 9, 7)),
Expand All @@ -16,3 +18,20 @@
def test_normalize_date(value, expected):
result = tslibs.normalize_date(value)
assert result == expected


class SubDatetime(datetime):
pass


@pytest.mark.parametrize("dt, expected", [
pytest.param(Timestamp(2000, 1, 1, 1),
Timestamp(2000, 1, 1, 0)),
pytest.param(datetime(2000, 1, 1, 1),
datetime(2000, 1, 1, 0)),
pytest.param(SubDatetime(2000, 1, 1, 1),
SubDatetime(2000, 1, 1, 0))])
def test_normalize_date_sub_types(dt, expected):
# GH 25851
result = tslibs.normalize_date(dt)
assert result == expected