Skip to content

BUG: Fix localize_pydatetime using meta datetimes as Timestamp (#25734) #25746

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

Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
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 @@ -136,7 +136,7 @@ Categorical
Datetimelike
^^^^^^^^^^^^

-
- Bug with :class:`Timestamp` and :class:`CustomBusinessDay` arithmatic throwing an exception with datetime subclasses (:issue:`25734`)
-
-

Expand Down
14 changes: 9 additions & 5 deletions pandas/_libs/tslibs/conversion.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -380,9 +380,11 @@ 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:
pass

if nanos:
obj.value += nanos
Expand Down Expand Up @@ -608,8 +610,10 @@ 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:
pass
elif is_utc(tz):
return _localize_pydatetime(dt, tz)
try:
Expand Down
33 changes: 33 additions & 0 deletions pandas/tests/arithmetic/test_datetime64.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime, timedelta
from itertools import product, starmap
import operator
import sys
import warnings

import numpy as np
Expand All @@ -21,6 +22,7 @@
DatetimeIndex, NaT, Period, Series, Timedelta, TimedeltaIndex, Timestamp,
date_range)
from pandas.core.indexes.datetimes import _to_M8
from pandas.tseries.offsets import CustomBusinessDay
import pandas.util.testing as tm


Expand Down Expand Up @@ -2350,3 +2352,34 @@ 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
Copy link
Contributor Author

@ArtificialQualia ArtificialQualia Mar 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A metaclass is needed in this test case to be able to override __instancecheck__


with monkeypatch.context() as m:
# monkeypatch datetime everywhere
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you expand this comment to describe what freezegun (and other libraries) are doing?

for mod_name, module in list(sys.modules.items()):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least some level of monkeypatching is necessary for the original issue to show up.

However, this entire for block could be replaced with m.setattr("pandas.tseries.offsets.datetime", FakeDatetime), but I wanted to future proof this test better and replicate what freezegun and other libraries that might monkeypatch datetime would do.

if (mod_name == __name__ or
module.__name__ in ('datetime',)):
continue
for attribute_name in dir(module):
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
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 date_range, Timestamp
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 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