Skip to content

BUG: Timestamp fails when fold is passed with positional args #52221

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
1 change: 1 addition & 0 deletions doc/source/whatsnew/v2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ Datetimelike
- Bug in :meth:`Timestamp.round` with values close to the implementation bounds returning incorrect results instead of raising ``OutOfBoundsDatetime`` (:issue:`51494`)
- :meth:`arrays.DatetimeArray.map` can now take a ``na_action`` argument. :meth:`DatetimeIndex.map` with ``na_action="ignore"`` now works as expected. (:issue:`51644`)
- Bug in :meth:`arrays.DatetimeArray.map` and :meth:`DatetimeIndex.map`, where the supplied callable operated array-wise instead of element-wise (:issue:`51977`)
- Bug in :class:`Timestamp` raising an error when passing fold when constructing from positional arguments.
-

Timedelta
Expand Down
16 changes: 16 additions & 0 deletions pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1269,7 +1269,23 @@ cdef class _Timestamp(ABCTimestamp):
# Python front end to C extension type _Timestamp
# This serves as the box for datetime64

def _fix_positional_arguments(cls):
original_new = cls.__new__

def updated_new(cls, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

a decorator seems strictly worse than just changing __new__ directly

Copy link
Member Author

@AlexKirko AlexKirko Mar 31, 2023

Choose a reason for hiding this comment

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

I am happy to change __new__ directly if anyone has a suggestion how to tell the difference between the two calls above:

pd.Timestamp(2023, 3, 30, 10, None, None, 30)
pd.Timestamp(2023, 3, 30, 10, second=30)

If we conclude that the only acceptable way to fix this bug is to refactor the function to take in *args, **kwargs right now, I can close this PR, open a new issue with all the bugs currently present because of the broken variable contents, and work on refactoring.

What I am against is doing a patchwork fix inside __new__ that leaves the variable contents with the wrong arguments, because it turns out to bring a bunch of bugs.

Copy link
Member Author

Choose a reason for hiding this comment

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

My suggestion is to fix this right now with a decorator, then I open a new issue, refactor the function and make this PR obsolete in a few weeks to a month it will take to refactor and discuss the implementation.

I'm fine with skipping this easy, costly performance-wise, short-term bugfix if you guys aren't comfortable.

Copy link
Member

Choose a reason for hiding this comment

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

I think it makes sense to jump directly to implementing the args, kwargs solution then if it's the strategy being considered for the original issue

Copy link
Member Author

@AlexKirko AlexKirko Mar 31, 2023

Choose a reason for hiding this comment

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

Okay, I'll close the PR and open the new issue tomorrow then. Please note that this causes bugs on 1.5.x too, not just on 2.x.

# GH#52117 If we passed positional args, then _ts_input
# is now set to year and the other positional args
# are shifted to the left. Let's shift back
if len(args) > 2 and all(isinstance(arg, int) for arg in args[:3]):
return original_new(cls, _no_input, *args, **kwargs)
else:
return original_new(cls, *args, **kwargs)

cls.__new__ = updated_new
return cls


@_fix_positional_arguments
class Timestamp(_Timestamp):
"""
Pandas replacement for python datetime.datetime object.
Expand Down
25 changes: 19 additions & 6 deletions pandas/tests/scalar/timestamp/test_constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,12 +340,7 @@ def test_constructor_positional_with_tzinfo(self):

@pytest.mark.parametrize("kwd", ["nanosecond", "microsecond", "second", "minute"])
def test_constructor_positional_keyword_mixed_with_tzinfo(self, kwd, request):
# TODO: if we passed microsecond with a keyword we would mess up
# xref GH#45307
if kwd != "nanosecond":
# nanosecond is keyword-only as of 2.0, others are not
mark = pytest.mark.xfail(reason="GH#45307")
request.node.add_marker(mark)
# GH#52221 makes a mix of positional and keyword arguments behave consistently

kwargs = {kwd: 4}
ts = Timestamp(2020, 12, 31, tzinfo=timezone.utc, **kwargs)
Expand Down Expand Up @@ -899,3 +894,21 @@ def test_timestamp_constructor_adjust_value_for_fold(tz, ts_input, fold, value_o
result = ts._value
expected = value_out
assert result == expected


@pytest.mark.parametrize("tz", ["dateutil/Europe/London"])
def test_timestamp_constructor_positional_with_fold(tz):
# Check that we build an object successfully
# if we pass positional arguments and fold
ts = Timestamp(2019, 10, 27, 1, 30, tz=tz, fold=0)
result = ts._value
expected = 1572136200000000
assert result == expected


def test_timestamp_constructor_arg_shift():
# Check that passing a positional argument as keyword
# does not change the value
result = Timestamp(2019, 10, 27, minute=30)
expected = Timestamp(2019, 10, 27, 0, 30)
assert result == expected