Skip to content

Commit 7e76ad1

Browse files
committed
REF (feedback): move maybe_box_datetimelike common.py -> dtypes/cast.py
2 parents 748be1b + 1e7f147 commit 7e76ad1

28 files changed

+397
-470
lines changed

doc/source/whatsnew/v1.2.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,7 @@ Indexing
456456
- Bug in :meth:`Series.loc.__getitem__` with a non-unique :class:`MultiIndex` and an empty-list indexer (:issue:`13691`)
457457
- Bug in indexing on a :class:`Series` or :class:`DataFrame` with a :class:`MultiIndex` with a level named "0" (:issue:`37194`)
458458
- Bug in :meth:`Series.__getitem__` when using an unsigned integer array as an indexer giving incorrect results or segfaulting instead of raising ``KeyError`` (:issue:`37218`)
459+
- Bug in :meth:`Index.where` incorrectly casting numeric values to strings (:issue:`37591`)
459460

460461
Missing
461462
^^^^^^^

pandas/conftest.py

+10
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,16 @@ def unique_nulls_fixture(request):
290290
# ----------------------------------------------------------------
291291
# Classes
292292
# ----------------------------------------------------------------
293+
294+
295+
@pytest.fixture(params=[pd.DataFrame, pd.Series])
296+
def frame_or_series(request):
297+
"""
298+
Fixture to parametrize over DataFrame and Series.
299+
"""
300+
return request.param
301+
302+
293303
@pytest.fixture(
294304
params=[pd.Index, pd.Series], ids=["index", "series"] # type: ignore[list-item]
295305
)

pandas/core/computation/align.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ def _align_core_single_unary_op(
3838
def _zip_axes_from_type(
3939
typ: Type[FrameOrSeries], new_axes: Sequence[int]
4040
) -> Dict[str, int]:
41-
axes = {name: new_axes[i] for i, name in enumerate(typ._AXIS_ORDERS)}
42-
return axes
41+
return {name: new_axes[i] for i, name in enumerate(typ._AXIS_ORDERS)}
4342

4443

4544
def _any_pandas_objects(terms) -> bool:
@@ -186,8 +185,11 @@ def reconstruct_object(typ, obj, axes, dtype):
186185
# The condition is to distinguish 0-dim array (returned in case of
187186
# scalar) and 1 element array
188187
# e.g. np.array(0) and np.array([0])
189-
if len(obj.shape) == 1 and len(obj) == 1:
190-
if not isinstance(ret_value, np.ndarray):
191-
ret_value = np.array([ret_value]).astype(res_t)
188+
if (
189+
len(obj.shape) == 1
190+
and len(obj) == 1
191+
and not isinstance(ret_value, np.ndarray)
192+
):
193+
ret_value = np.array([ret_value]).astype(res_t)
192194

193195
return ret_value

pandas/core/computation/eval.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,11 @@ def _check_engine(engine: Optional[str]) -> str:
5252
# TODO: validate this in a more general way (thinking of future engines
5353
# that won't necessarily be import-able)
5454
# Could potentially be done on engine instantiation
55-
if engine == "numexpr":
56-
if not NUMEXPR_INSTALLED:
57-
raise ImportError(
58-
"'numexpr' is not installed or an unsupported version. Cannot use "
59-
"engine='numexpr' for query/eval if 'numexpr' is not installed"
60-
)
55+
if engine == "numexpr" and not NUMEXPR_INSTALLED:
56+
raise ImportError(
57+
"'numexpr' is not installed or an unsupported version. Cannot use "
58+
"engine='numexpr' for query/eval if 'numexpr' is not installed"
59+
)
6160

6261
return engine
6362

pandas/core/computation/expr.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -496,15 +496,14 @@ def _maybe_evaluate_binop(
496496
f"'{lhs.type}' and '{rhs.type}'"
497497
)
498498

499-
if self.engine != "pytables":
500-
if (
501-
res.op in CMP_OPS_SYMS
502-
and getattr(lhs, "is_datetime", False)
503-
or getattr(rhs, "is_datetime", False)
504-
):
505-
# all date ops must be done in python bc numexpr doesn't work
506-
# well with NaT
507-
return self._maybe_eval(res, self.binary_ops)
499+
if self.engine != "pytables" and (
500+
res.op in CMP_OPS_SYMS
501+
and getattr(lhs, "is_datetime", False)
502+
or getattr(rhs, "is_datetime", False)
503+
):
504+
# all date ops must be done in python bc numexpr doesn't work
505+
# well with NaT
506+
return self._maybe_eval(res, self.binary_ops)
508507

509508
if res.op in eval_in_python:
510509
# "in"/"not in" ops are always evaluated in python

pandas/core/computation/pytables.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -378,14 +378,14 @@ def prune(self, klass):
378378
operand = self.operand
379379
operand = operand.prune(klass)
380380

381-
if operand is not None:
382-
if issubclass(klass, ConditionBinOp):
383-
if operand.condition is not None:
384-
return operand.invert()
385-
elif issubclass(klass, FilterBinOp):
386-
if operand.filter is not None:
387-
return operand.invert()
388-
381+
if operand is not None and (
382+
issubclass(klass, ConditionBinOp)
383+
and operand.condition is not None
384+
or not issubclass(klass, ConditionBinOp)
385+
and issubclass(klass, FilterBinOp)
386+
and operand.filter is not None
387+
):
388+
return operand.invert()
389389
return None
390390

391391

pandas/core/computation/scope.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,7 @@ def __init__(
144144
def __repr__(self) -> str:
145145
scope_keys = _get_pretty_string(list(self.scope.keys()))
146146
res_keys = _get_pretty_string(list(self.resolvers.keys()))
147-
unicode_str = f"{type(self).__name__}(scope={scope_keys}, resolvers={res_keys})"
148-
return unicode_str
147+
return f"{type(self).__name__}(scope={scope_keys}, resolvers={res_keys})"
149148

150149
@property
151150
def has_resolvers(self) -> bool:

pandas/core/frame.py

+13-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,13 @@
132132
from pandas.core.construction import extract_array
133133
from pandas.core.generic import NDFrame, _shared_docs
134134
from pandas.core.indexes import base as ibase
135-
from pandas.core.indexes.api import Index, ensure_index, ensure_index_from_sequences
135+
from pandas.core.indexes.api import (
136+
DatetimeIndex,
137+
Index,
138+
PeriodIndex,
139+
ensure_index,
140+
ensure_index_from_sequences,
141+
)
136142
from pandas.core.indexes.multi import MultiIndex, maybe_droplevels
137143
from pandas.core.indexing import check_bool_indexer, convert_to_index_sliceable
138144
from pandas.core.internals import BlockManager
@@ -9254,6 +9260,9 @@ def to_timestamp(
92549260

92559261
axis_name = self._get_axis_name(axis)
92569262
old_ax = getattr(self, axis_name)
9263+
if not isinstance(old_ax, PeriodIndex):
9264+
raise TypeError(f"unsupported Type {type(old_ax).__name__}")
9265+
92579266
new_ax = old_ax.to_timestamp(freq=freq, how=how)
92589267

92599268
setattr(new_obj, axis_name, new_ax)
@@ -9283,6 +9292,9 @@ def to_period(self, freq=None, axis: Axis = 0, copy: bool = True) -> DataFrame:
92839292

92849293
axis_name = self._get_axis_name(axis)
92859294
old_ax = getattr(self, axis_name)
9295+
if not isinstance(old_ax, DatetimeIndex):
9296+
raise TypeError(f"unsupported Type {type(old_ax).__name__}")
9297+
92869298
new_ax = old_ax.to_period(freq=freq)
92879299

92889300
setattr(new_obj, axis_name, new_ax)

pandas/core/indexes/base.py

+5-13
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
ensure_int64,
4141
ensure_object,
4242
ensure_platform_int,
43-
is_bool,
4443
is_bool_dtype,
4544
is_categorical_dtype,
4645
is_datetime64_any_dtype,
@@ -4079,23 +4078,16 @@ def where(self, cond, other=None):
40794078
if other is None:
40804079
other = self._na_value
40814080

4082-
dtype = self.dtype
40834081
values = self.values
40844082

4085-
if is_bool(other) or is_bool_dtype(other):
4086-
4087-
# bools force casting
4088-
values = values.astype(object)
4089-
dtype = None
4083+
try:
4084+
self._validate_fill_value(other)
4085+
except (ValueError, TypeError):
4086+
return self.astype(object).where(cond, other)
40904087

40914088
values = np.where(cond, values, other)
40924089

4093-
if self._is_numeric_dtype and np.any(isna(values)):
4094-
# We can't coerce to the numeric dtype of "self" (unless
4095-
# it's float) if there are NaN values in our output.
4096-
dtype = None
4097-
4098-
return Index(values, dtype=dtype, name=self.name)
4090+
return Index(values, name=self.name)
40994091

41004092
# construction helpers
41014093
@final

pandas/core/indexes/datetimelike.py

+2-9
Original file line numberDiff line numberDiff line change
@@ -482,16 +482,9 @@ def isin(self, values, level=None):
482482

483483
@Appender(Index.where.__doc__)
484484
def where(self, cond, other=None):
485-
values = self._data._ndarray
485+
other = self._data._validate_setitem_value(other)
486486

487-
try:
488-
other = self._data._validate_setitem_value(other)
489-
except (TypeError, ValueError) as err:
490-
# Includes tzawareness mismatch and IncompatibleFrequencyError
491-
oth = getattr(other, "dtype", other)
492-
raise TypeError(f"Where requires matching dtype, not {oth}") from err
493-
494-
result = np.where(cond, values, other)
487+
result = np.where(cond, self._data._ndarray, other)
495488
arr = self._data._from_backing_data(result)
496489
return type(self)._simple_new(arr, name=self.name)
497490

pandas/core/indexes/numeric.py

+2
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ def _validate_fill_value(self, value):
121121
# force conversion to object
122122
# so we don't lose the bools
123123
raise TypeError
124+
if isinstance(value, str):
125+
raise TypeError
124126

125127
return value
126128

pandas/tests/frame/methods/test_at_time.py

+33-12
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,57 @@
44
import pytest
55
import pytz
66

7+
from pandas._libs.tslibs import timezones
8+
79
from pandas import DataFrame, date_range
810
import pandas._testing as tm
911

1012

1113
class TestAtTime:
12-
def test_at_time(self):
14+
@pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"])
15+
def test_localized_at_time(self, tzstr, frame_or_series):
16+
tz = timezones.maybe_get_tz(tzstr)
17+
18+
rng = date_range("4/16/2012", "5/1/2012", freq="H")
19+
ts = frame_or_series(np.random.randn(len(rng)), index=rng)
20+
21+
ts_local = ts.tz_localize(tzstr)
22+
23+
result = ts_local.at_time(time(10, 0))
24+
expected = ts.at_time(time(10, 0)).tz_localize(tzstr)
25+
tm.assert_equal(result, expected)
26+
assert timezones.tz_compare(result.index.tz, tz)
27+
28+
def test_at_time(self, frame_or_series):
1329
rng = date_range("1/1/2000", "1/5/2000", freq="5min")
1430
ts = DataFrame(np.random.randn(len(rng), 2), index=rng)
31+
if frame_or_series is not DataFrame:
32+
ts = ts[0]
1533
rs = ts.at_time(rng[1])
1634
assert (rs.index.hour == rng[1].hour).all()
1735
assert (rs.index.minute == rng[1].minute).all()
1836
assert (rs.index.second == rng[1].second).all()
1937

2038
result = ts.at_time("9:30")
2139
expected = ts.at_time(time(9, 30))
22-
tm.assert_frame_equal(result, expected)
23-
24-
result = ts.loc[time(9, 30)]
25-
expected = ts.loc[(rng.hour == 9) & (rng.minute == 30)]
26-
27-
tm.assert_frame_equal(result, expected)
40+
tm.assert_equal(result, expected)
2841

42+
def test_at_time_midnight(self, frame_or_series):
2943
# midnight, everything
3044
rng = date_range("1/1/2000", "1/31/2000")
3145
ts = DataFrame(np.random.randn(len(rng), 3), index=rng)
46+
if frame_or_series is not DataFrame:
47+
ts = ts[0]
3248

3349
result = ts.at_time(time(0, 0))
34-
tm.assert_frame_equal(result, ts)
50+
tm.assert_equal(result, ts)
3551

52+
def test_at_time_nonexistent(self, frame_or_series):
3653
# time doesn't exist
3754
rng = date_range("1/1/2012", freq="23Min", periods=384)
38-
ts = DataFrame(np.random.randn(len(rng), 2), rng)
55+
ts = DataFrame(np.random.randn(len(rng)), rng)
56+
if frame_or_series is not DataFrame:
57+
ts = ts[0]
3958
rs = ts.at_time("16:00")
4059
assert len(rs) == 0
4160

@@ -62,12 +81,14 @@ def test_at_time_tz(self):
6281
expected = df.iloc[1:2]
6382
tm.assert_frame_equal(result, expected)
6483

65-
def test_at_time_raises(self):
84+
def test_at_time_raises(self, frame_or_series):
6685
# GH#20725
67-
df = DataFrame([[1, 2, 3], [4, 5, 6]])
86+
obj = DataFrame([[1, 2, 3], [4, 5, 6]])
87+
if frame_or_series is not DataFrame:
88+
obj = obj[0]
6889
msg = "Index must be DatetimeIndex"
6990
with pytest.raises(TypeError, match=msg): # index is not a DatetimeIndex
70-
df.at_time("00:00")
91+
obj.at_time("00:00")
7192

7293
@pytest.mark.parametrize("axis", ["index", "columns", 0, 1])
7394
def test_at_time_axis(self, axis):

0 commit comments

Comments
 (0)