diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index ddc5e543c6165..8dbfa2a898694 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -136,7 +136,7 @@ Categorical Datetimelike ^^^^^^^^^^^^ -- +- Bug with :class:`Timestamp` and :class:`CustomBusinessDay` arithmetic throwing an exception with datetime subclasses (:issue:`25734`) - - diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index 1c0adaaa288a9..5c1ea95676897 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -380,9 +380,12 @@ cdef _TSObject convert_datetime_to_tsobject(datetime ts, object tz, obj.value -= int(offset.total_seconds() * 1e9) if not PyDateTime_CheckExact(ts): - # datetime instance but not datetime type --> Timestamp - obj.value += ts.nanosecond - obj.dts.ps = ts.nanosecond * 1000 + try: + obj.value += ts.nanosecond + obj.dts.ps = ts.nanosecond * 1000 + except AttributeError: + # probably a subclass of datetime + pass if nanos: obj.value += nanos @@ -608,8 +611,11 @@ 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 - return dt.tz_localize(tz) + try: + return dt.tz_localize(tz) + except AttributeError: + # probably a subclass of datetime + pass elif is_utc(tz): return _localize_pydatetime(dt, tz) try: diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index c81a371f37dc1..4ee635025fbe1 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta from itertools import product, starmap import operator +import sys import warnings import numpy as np @@ -23,6 +24,8 @@ from pandas.core.indexes.datetimes import _to_M8 import pandas.util.testing as tm +from pandas.tseries.offsets import CustomBusinessDay + def assert_all(obj): """ @@ -2350,3 +2353,38 @@ def test_shift_months(years, months): for x in dti] expected = DatetimeIndex(raw) tm.assert_index_equal(actual, expected) + + +def test_add_with_monkeypatched_datetime(monkeypatch): + # GH 25734 + + class MetaDatetime(type): + @classmethod + def __instancecheck__(self, obj): + return isinstance(obj, datetime) + + class FakeDatetime(MetaDatetime("NewBase", (datetime,), {})): + pass + + with monkeypatch.context() as m: + # monkeypatch datetime everywhere + for mod_name, module in list(sys.modules.items()): + try: + if (mod_name == __name__ or + module.__name__ in ('datetime',)): + continue + module_attributes = dir(module) + except (ImportError, AttributeError, TypeError): + continue + for attribute_name in module_attributes: + try: + attribute_value = getattr(module, attribute_name) + except (ImportError, AttributeError, TypeError): + continue + if id(datetime) == id(attribute_value): + m.setattr(module, attribute_name, FakeDatetime) + + dt = FakeDatetime(2000, 1, 1, tzinfo=pytz.UTC) + result = Timestamp(dt) + CustomBusinessDay() + expected = Timestamp("2000-01-03", tzinfo=pytz.UTC) + assert result == expected diff --git a/pandas/tests/tslibs/test_conversion.py b/pandas/tests/tslibs/test_conversion.py index 13398a69b4982..4b4345da18d1c 100644 --- a/pandas/tests/tslibs/test_conversion.py +++ b/pandas/tests/tslibs/test_conversion.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from datetime import datetime + import numpy as np import pytest from pytz import UTC @@ -7,7 +9,7 @@ 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 @@ -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 FakeDatetime(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(FakeDatetime(2000, 1, 1), + FakeDatetime(2000, 1, 1, tzinfo=UTC), + id="fakedatetime")]) +def test_localize_pydatetime_dt_types(dt, expected): + # GH 25734 + result = conversion.localize_pydatetime(dt, UTC) + assert result == expected