Repository: pandas-dev/pandas (python)

Issue:
TST: Refactor ``test_subtracting_two_series_with_unordered_index_and_all_nan_index``
The test is hard to read and hard to understand. We should refactor it to make it more straightforward without those parametrizations. Pending #49442

Pull request:
CLN/TST: test_series_align_multiindex_containing_nan
- [x] closes #49443
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added [type annotations](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#type-hints) to new arguments/methods/functions.


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 82d8fc12c5..6a840ebfb9 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -95,7 +95,6 @@ repos:
             |^pandas/util/_test_decorators\.py  # keep excluded
             |^pandas/_version\.py  # keep excluded
             |^pandas/conftest\.py  # keep excluded
-            |^pandas/core/generic\.py
         args: [--disable=all, --enable=redefined-outer-name]
         stages: [manual]
 -   repo: https://github.com/PyCQA/isort
diff --git a/doc/source/reference/arrays.rst b/doc/source/reference/arrays.rst
index 5b41de4e12..aeaca7caea 100644
--- a/doc/source/reference/arrays.rst
+++ b/doc/source/reference/arrays.rst
@@ -60,6 +60,37 @@ is an :class:`ArrowDtype`.
 `Pyarrow <https://arrow.apache.org/docs/python/index.html>`__ provides similar array and `data type <https://arrow.apache.org/docs/python/api/datatypes.html>`__
 support as NumPy including first-class nullability support for all data types, immutability and more.
 
+The table below shows the equivalent pyarrow-backed (``pa``), pandas extension, and numpy (``np``) types that are recognized by pandas.
+Pyarrow-backed types below need to be passed into :class:`ArrowDtype` to be recognized by pandas e.g. ``pd.ArrowDtype(pa.bool_())``
+
+=============================================== ========================== ===================
+PyArrow type                                    pandas extension type      NumPy type
+=============================================== ========================== ===================
+:external+pyarrow:py:func:`pyarrow.bool_`       :class:`BooleanDtype`      ``np.bool_``
+:external+pyarrow:py:func:`pyarrow.int8`        :class:`Int8Dtype`         ``np.int8``
+:external+pyarrow:py:func:`pyarrow.int16`       :class:`Int16Dtype`        ``np.int16``
+:external+pyarrow:py:func:`pyarrow.int32`       :class:`Int32Dtype`        ``np.int32``
+:external+pyarrow:py:func:`pyarrow.int64`       :class:`Int64Dtype`        ``np.int64``
+:external+pyarrow:py:func:`pyarrow.uint8`       :class:`UInt8Dtype`        ``np.uint8``
+:external+pyarrow:py:func:`pyarrow.uint16`      :class:`UInt16Dtype`       ``np.uint16``
+:external+pyarrow:py:func:`pyarrow.uint32`      :class:`UInt32Dtype`       ``np.uint32``
+:external+pyarrow:py:func:`pyarrow.uint64`      :class:`UInt64Dtype`       ``np.uint64``
+:external+pyarrow:py:func:`pyarrow.float32`     :class:`Float32Dtype`      ``np.float32``
+:external+pyarrow:py:func:`pyarrow.float64`     :class:`Float64Dtype`      ``np.float64``
+:external+pyarrow:py:func:`pyarrow.time32`      (none)                     (none)
+:external+pyarrow:py:func:`pyarrow.time64`      (none)                     (none)
+:external+pyarrow:py:func:`pyarrow.timestamp`   :class:`DatetimeTZDtype`   ``np.datetime64``
+:external+pyarrow:py:func:`pyarrow.date32`      (none)                     (none)
+:external+pyarrow:py:func:`pyarrow.date64`      (none)                     (none)
+:external+pyarrow:py:func:`pyarrow.duration`    (none)                     ``np.timedelta64``
+:external+pyarrow:py:func:`pyarrow.binary`      (none)                     (none)
+:external+pyarrow:py:func:`pyarrow.string`      :class:`StringDtype`       ``np.str_``
+:external+pyarrow:py:func:`pyarrow.decimal128`  (none)                     (none)
+:external+pyarrow:py:func:`pyarrow.list_`       (none)                     (none)
+:external+pyarrow:py:func:`pyarrow.map_`        (none)                     (none)
+:external+pyarrow:py:func:`pyarrow.dictionary`  :class:`CategoricalDtype`  (none)
+=============================================== ========================== ===================
+
 .. note::
 
     For string types (``pyarrow.string()``, ``string[pyarrow]``), PyArrow support is still facilitated
diff --git a/doc/source/whatsnew/v2.0.0.rst b/doc/source/whatsnew/v2.0.0.rst
index 13b60f7f05..abaef639d6 100644
--- a/doc/source/whatsnew/v2.0.0.rst
+++ b/doc/source/whatsnew/v2.0.0.rst
@@ -462,7 +462,7 @@ to each element individually, e.g. ::
 
 Other API changes
 ^^^^^^^^^^^^^^^^^
-- The ``freq``, ``tz``, ``nanosecond``, and ``unit`` keywords in the :class:`Timestamp` constructor are now keyword-only (:issue:`45307`)
+- The ``freq``, ``tz``, ``nanosecond``, and ``unit`` keywords in the :class:`Timestamp` constructor are now keyword-only (:issue:`45307`, :issue:`32526`)
 - Passing ``nanoseconds`` greater than 999 or less than 0 in :class:`Timestamp` now raises a ``ValueError`` (:issue:`48538`, :issue:`48255`)
 - :func:`read_csv`: specifying an incorrect number of columns with ``index_col`` of now raises ``ParserError`` instead of ``IndexError`` when using the c parser.
 - Default value of ``dtype`` in :func:`get_dummies` is changed to ``bool`` from ``uint8`` (:issue:`45848`)
@@ -821,7 +821,7 @@ Datetimelike
 - Bug in :func:`to_datetime` was throwing ``ValueError`` when parsing dates with ISO8601 format where some values were not zero-padded (:issue:`21422`)
 - Bug in :func:`to_datetime` was giving incorrect results when using ``format='%Y%m%d'`` and ``errors='ignore'`` (:issue:`26493`)
 - Bug in :func:`to_datetime` was failing to parse date strings ``'today'`` and ``'now'`` if ``format`` was not ISO8601 (:issue:`50359`)
--
+- Bug in :func:`Timestamp.utctimetuple` raising a ``TypeError`` (:issue:`32174`)
 
 Timedelta
 ^^^^^^^^^
@@ -881,6 +881,7 @@ Indexing
 - Bug in :func:`~DataFrame.describe` when formatting percentiles in the resulting index showed more decimals than needed (:issue:`46362`)
 - Bug in :meth:`DataFrame.compare` does not recognize differences when comparing ``NA`` with value in nullable dtypes (:issue:`48939`)
 - Bug in :meth:`DataFrame.isetitem` coercing extension array dtypes in :class:`DataFrame` to object (:issue:`49922`)
+- Bug in :class:`BusinessHour` would cause creation of :class:`DatetimeIndex` to fail when no opening hour was included in the index (:issue:`49835`)
 -
 
 Missing
@@ -951,6 +952,7 @@ Groupby/resample/rolling
 - Bug in :meth:`.SeriesGroupBy.nth` would raise when grouper contained NA values after subsetting from a :class:`DataFrameGroupBy` (:issue:`26454`)
 - Bug in :meth:`DataFrame.groupby` would not include a :class:`.Grouper` specified by ``key`` in the result when ``as_index=False`` (:issue:`50413`)
 - Bug in :meth:`.DataFrameGrouBy.value_counts` would raise when used with a :class:`.TimeGrouper` (:issue:`50486`)
+- Bug in :meth:`Resampler.size` caused a wide :class:`DataFrame` to be returned instead of a :class:`Series` with :class:`MultiIndex` (:issue:`46826`)
 -
 
 Reshaping
diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx
index 0bc9751694..b59d17321d 100644
--- a/pandas/_libs/tslibs/offsets.pyx
+++ b/pandas/_libs/tslibs/offsets.pyx
@@ -1847,15 +1847,20 @@ cdef class BusinessHour(BusinessMixin):
         earliest_start = self.start[0]
         latest_start = self.start[-1]
 
+        if self.n == 0:
+            is_same_sign = sign > 0
+        else:
+            is_same_sign = self.n * sign >= 0
+
         if not self.next_bday.is_on_offset(other):
             # today is not business day
             other = other + sign * self.next_bday
-            if self.n * sign >= 0:
+            if is_same_sign:
                 hour, minute = earliest_start.hour, earliest_start.minute
             else:
                 hour, minute = latest_start.hour, latest_start.minute
         else:
-            if self.n * sign >= 0:
+            if is_same_sign:
                 if latest_start < other.time():
                     # current time is after latest starting time in today
                     other = other + sign * self.next_bday
diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx
index 0cef0ad128..b57f2ce5bd 100644
--- a/pandas/_libs/tslibs/timestamps.pyx
+++ b/pandas/_libs/tslibs/timestamps.pyx
@@ -1525,16 +1525,11 @@ class Timestamp(_Timestamp):
         elif is_integer_object(year):
             # User passed positional arguments:
             # Timestamp(year, month, day[, hour[, minute[, second[,
-            # microsecond[, nanosecond[, tzinfo]]]]]])
+            # microsecond[, tzinfo]]]]])
             ts_input = datetime(ts_input, year, month, day or 0,
                                 hour or 0, minute or 0, second or 0, fold=fold or 0)
             unit = None
 
-            if nanosecond is None:
-                # nanosecond was not passed as a keyword, but may have been
-                #  passed positionally see test_constructor_nanosecond
-                nanosecond = microsecond
-
         if getattr(ts_input, "tzinfo", None) is not None and tz is not None:
             raise ValueError("Cannot pass a datetime or Timestamp with tzinfo with "
                              "the tz parameter. Use tz_convert instead.")
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
index 5a04c0a68d..3517f3ee91 100644
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -142,7 +142,6 @@ from pandas.core.array_algos.replace import should_use_regex
 from pandas.core.arrays import ExtensionArray
 from pandas.core.base import PandasObject
 from pandas.core.construction import extract_array
-from pandas.core.describe import describe_ndframe
 from pandas.core.flags import Flags
 from pandas.core.indexes.api import (
     DatetimeIndex,
@@ -159,6 +158,8 @@ from pandas.core.internals import (
     SingleArrayManager,
 )
 from pandas.core.internals.construction import mgr_to_mgr
+from pandas.core.internals.managers import _using_copy_on_write
+from pandas.core.methods.describe import describe_ndframe
 from pandas.core.missing import (
     clean_fill_method,
     clean_reindex_fill_method,
@@ -9950,7 +9951,7 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         before=None,
         after=None,
         axis: Axis | None = None,
-        copy: bool_t = True,
+        copy: bool_t | None = None,
     ) -> NDFrameT:
         """
         Truncate a Series or DataFrame before and after some index value.
@@ -10101,8 +10102,8 @@ class NDFrame(PandasObject, indexing.IndexingMixin):
         if isinstance(ax, MultiIndex):
             setattr(result, self._get_axis_name(axis), ax.truncate(before, after))
 
-        if copy:
-            result = result.copy()
+        if copy or (copy is None and not _using_copy_on_write()):
+            result = result.copy(deep=copy)
 
         return result
 
diff --git a/pandas/core/describe.py b/pandas/core/methods/describe.py
similarity index 100%
rename from pandas/core/describe.py
rename to pandas/core/methods/describe.py
diff --git a/pandas/core/resample.py b/pandas/core/resample.py
index 4af4be20a0..d6cba82476 100644
--- a/pandas/core/resample.py
+++ b/pandas/core/resample.py
@@ -989,6 +989,12 @@ class Resampler(BaseGroupBy, PandasObject):
     @doc(GroupBy.size)
     def size(self):
         result = self._downsample("size")
+
+        # If the result is a non-empty DataFrame we stack to get a Series
+        # GH 46826
+        if isinstance(result, ABCDataFrame) and not result.empty:
+            result = result.stack()
+
         if not len(self.ax):
             from pandas import Series
 
diff --git a/pandas/io/formats/printing.py b/pandas/io/formats/printing.py
index a12e62223c..9b150c2396 100644
--- a/pandas/io/formats/printing.py
+++ b/pandas/io/formats/printing.py
@@ -259,9 +259,10 @@ def enable_data_resource_formatter(enable: bool) -> None:
         if mimetype not in formatters:
             # define tableschema formatter
             from IPython.core.formatters import BaseFormatter
+            from traitlets import ObjectName
 
             class TableSchemaFormatter(BaseFormatter):
-                print_method = "_repr_data_resource_"
+                print_method = ObjectName("_repr_data_resource_")
                 _return_type = (dict,)
 
             # register it:


Test patch:
diff --git a/pandas/tests/copy_view/test_methods.py b/pandas/tests/copy_view/test_methods.py
index bac7d09389..63b0be281f 100644
--- a/pandas/tests/copy_view/test_methods.py
+++ b/pandas/tests/copy_view/test_methods.py
@@ -371,6 +371,30 @@ def test_head_tail(method, using_copy_on_write):
     tm.assert_frame_equal(df, df_orig)
 
 
+@pytest.mark.parametrize(
+    "kwargs",
+    [
+        {"before": "a", "after": "b", "axis": 1},
+        {"before": 0, "after": 1, "axis": 0},
+    ],
+)
+def test_truncate(using_copy_on_write, kwargs):
+    df = DataFrame({"a": [1, 2, 3], "b": 1, "c": 2})
+    df_orig = df.copy()
+    df2 = df.truncate(**kwargs)
+    df2._mgr._verify_integrity()
+
+    if using_copy_on_write:
+        assert np.shares_memory(get_array(df2, "a"), get_array(df, "a"))
+    else:
+        assert not np.shares_memory(get_array(df2, "a"), get_array(df, "a"))
+
+    df2.iloc[0, 0] = 0
+    if using_copy_on_write:
+        assert not np.shares_memory(get_array(df2, "a"), get_array(df, "a"))
+    tm.assert_frame_equal(df, df_orig)
+
+
 @pytest.mark.parametrize("method", ["assign", "drop_duplicates"])
 def test_assign_drop_duplicates(using_copy_on_write, method):
     df = DataFrame({"a": [1, 2, 3]})
diff --git a/pandas/tests/frame/indexing/test_mask.py b/pandas/tests/frame/indexing/test_mask.py
index e8a49ab868..23458b096a 100644
--- a/pandas/tests/frame/indexing/test_mask.py
+++ b/pandas/tests/frame/indexing/test_mask.py
@@ -7,6 +7,7 @@ import numpy as np
 from pandas import (
     NA,
     DataFrame,
+    Float64Dtype,
     Series,
     StringDtype,
     Timedelta,
@@ -130,3 +131,13 @@ def test_mask_where_dtype_timedelta():
         [np.nan, np.nan, np.nan, Timedelta("3 day"), Timedelta("4 day")]
     )
     tm.assert_frame_equal(df.where(df > Timedelta(2, unit="d")), expected)
+
+
+def test_mask_return_dtype():
+    # GH#50488
+    ser = Series([0.0, 1.0, 2.0, 3.0], dtype=Float64Dtype())
+    cond = ~ser.isna()
+    other = Series([True, False, True, False])
+    excepted = Series([1.0, 0.0, 1.0, 0.0], dtype=ser.dtype)
+    result = ser.mask(cond, other)
+    tm.assert_series_equal(result, excepted)
diff --git a/pandas/tests/frame/methods/test_truncate.py b/pandas/tests/frame/methods/test_truncate.py
index bfee3edc08..21f0664707 100644
--- a/pandas/tests/frame/methods/test_truncate.py
+++ b/pandas/tests/frame/methods/test_truncate.py
@@ -66,12 +66,6 @@ class TestDataFrameTruncate:
                 before=ts.index[-1] - ts.index.freq, after=ts.index[0] + ts.index.freq
             )
 
-    def test_truncate_copy(self, datetime_frame):
-        index = datetime_frame.index
-        truncated = datetime_frame.truncate(index[5], index[10])
-        truncated.values[:] = 5.0
-        assert not (datetime_frame.values[5:11] == 5).any()
-
     def test_truncate_nonsortedindex(self, frame_or_series):
         # GH#17935
 
diff --git a/pandas/tests/indexing/multiindex/test_multiindex.py b/pandas/tests/indexing/multiindex/test_multiindex.py
index f5f58e7e81..8e50721297 100644
--- a/pandas/tests/indexing/multiindex/test_multiindex.py
+++ b/pandas/tests/indexing/multiindex/test_multiindex.py
@@ -151,57 +151,35 @@ class TestMultiIndexBasic:
         expected = DataFrame(index=mi2)
         tm.assert_frame_equal(df, expected)
 
-    @pytest.mark.parametrize(
-        "data_result, data_expected",
-        [
-            (
-                [
-                    [(81.0, np.nan), (np.nan, np.nan)],
-                    [(np.nan, np.nan), (82.0, np.nan)],
-                    [1, 2],
-                    [1, 2],
-                ],
-                [
-                    [[81, 82.0, np.nan], Series([np.nan, np.nan, np.nan])],
-                    [[81, 82.0, np.nan], Series([np.nan, np.nan, np.nan])],
-                    [1, np.nan, 2],
-                    [np.nan, 2, 1],
-                ],
-            ),
-            (
-                [
-                    [(81.0, np.nan), (np.nan, np.nan)],
-                    [(np.nan, np.nan), (81.0, np.nan)],
-                    [1, 2],
-                    [1, 2],
-                ],
-                [
-                    [[81.0, np.nan], Series([np.nan, np.nan])],
-                    [[81.0, np.nan], Series([np.nan, np.nan])],
-                    [1, 2],
-                    [2, 1],
-                ],
-            ),
-        ],
-    )
-    def test_subtracting_two_series_with_unordered_index_and_all_nan_index(
-        self, data_result, data_expected
-    ):
+    def test_series_align_multiindex_with_nan_overlap_only(self):
+        # GH 38439
+        mi1 = MultiIndex.from_arrays([[81.0, np.nan], [np.nan, np.nan]])
+        mi2 = MultiIndex.from_arrays([[np.nan, 82.0], [np.nan, np.nan]])
+        ser1 = Series([1, 2], index=mi1)
+        ser2 = Series([1, 2], index=mi2)
+        result1, result2 = ser1.align(ser2)
+
+        mi = MultiIndex.from_arrays([[81.0, 82.0, np.nan], [np.nan, np.nan, np.nan]])
+        expected1 = Series([1.0, np.nan, 2.0], index=mi)
+        expected2 = Series([np.nan, 2.0, 1.0], index=mi)
+
+        tm.assert_series_equal(result1, expected1)
+        tm.assert_series_equal(result2, expected2)
+
+    def test_series_align_multiindex_with_nan(self):
         # GH 38439
-        # TODO: Refactor. This is impossible to understand GH#49443
-        a_index_result = MultiIndex.from_tuples(data_result[0])
-        b_index_result = MultiIndex.from_tuples(data_result[1])
-        a_series_result = Series(data_result[2], index=a_index_result)
-        b_series_result = Series(data_result[3], index=b_index_result)
-        result = a_series_result.align(b_series_result)
-
-        a_index_expected = MultiIndex.from_arrays(data_expected[0])
-        b_index_expected = MultiIndex.from_arrays(data_expected[1])
-        a_series_expected = Series(data_expected[2], index=a_index_expected)
-        b_series_expected = Series(data_expected[3], index=b_index_expected)
-
-        tm.assert_series_equal(result[0], a_series_expected)
-        tm.assert_series_equal(result[1], b_series_expected)
+        mi1 = MultiIndex.from_arrays([[81.0, np.nan], [np.nan, np.nan]])
+        mi2 = MultiIndex.from_arrays([[np.nan, 81.0], [np.nan, np.nan]])
+        ser1 = Series([1, 2], index=mi1)
+        ser2 = Series([1, 2], index=mi2)
+        result1, result2 = ser1.align(ser2)
+
+        mi = MultiIndex.from_arrays([[81.0, np.nan], [np.nan, np.nan]])
+        expected1 = Series([1, 2], index=mi)
+        expected2 = Series([2, 1], index=mi)
+
+        tm.assert_series_equal(result1, expected1)
+        tm.assert_series_equal(result2, expected2)
 
     def test_nunique_smoke(self):
         # GH 34019
diff --git a/pandas/tests/resample/test_resampler_grouper.py b/pandas/tests/resample/test_resampler_grouper.py
index 0432cf3970..a521e24aa6 100644
--- a/pandas/tests/resample/test_resampler_grouper.py
+++ b/pandas/tests/resample/test_resampler_grouper.py
@@ -515,3 +515,25 @@ def test_resample_empty_Dataframe(keys):
         expected.index.name = keys[0]
 
     tm.assert_frame_equal(result, expected)
+
+
+def test_groupby_resample_size_all_index_same():
+    # GH 46826
+    df = DataFrame(
+        {"A": [1] * 3 + [2] * 3 + [1] * 3 + [2] * 3, "B": np.arange(12)},
+        index=date_range("31/12/2000 18:00", freq="H", periods=12),
+    )
+    result = df.groupby("A").resample("D").size()
+    expected = Series(
+        3,
+        index=pd.MultiIndex.from_tuples(
+            [
+                (1, Timestamp("2000-12-31")),
+                (1, Timestamp("2001-01-01")),
+                (2, Timestamp("2000-12-31")),
+                (2, Timestamp("2001-01-01")),
+            ],
+            names=["A", None],
+        ),
+    )
+    tm.assert_series_equal(result, expected)
diff --git a/pandas/tests/scalar/timestamp/test_constructors.py b/pandas/tests/scalar/timestamp/test_constructors.py
index 604429e7c8..4143e52bbb 100644
--- a/pandas/tests/scalar/timestamp/test_constructors.py
+++ b/pandas/tests/scalar/timestamp/test_constructors.py
@@ -415,12 +415,13 @@ class TestTimestampConstructors:
                 nanosecond=1,
                 tz="UTC",
             ),
-            Timestamp(2000, 1, 2, 3, 4, 5, 6, 1, None),
-            Timestamp(2000, 1, 2, 3, 4, 5, 6, 1, pytz.UTC),
+            Timestamp(2000, 1, 2, 3, 4, 5, 6, None, nanosecond=1),
+            Timestamp(2000, 1, 2, 3, 4, 5, 6, tz=pytz.UTC, nanosecond=1),
         ],
     )
     def test_constructor_nanosecond(self, result):
         # GH 18898
+        # As of 2.0 (GH 49416), nanosecond should not be accepted positionally
         expected = Timestamp(datetime(2000, 1, 2, 3, 4, 5, 6), tz=result.tz)
         expected = expected + Timedelta(nanoseconds=1)
         assert result == expected
diff --git a/pandas/tests/scalar/timestamp/test_timestamp.py b/pandas/tests/scalar/timestamp/test_timestamp.py
index de41ea9021..70f9f7c924 100644
--- a/pandas/tests/scalar/timestamp/test_timestamp.py
+++ b/pandas/tests/scalar/timestamp/test_timestamp.py
@@ -7,6 +7,7 @@ from datetime import (
     timezone,
 )
 import locale
+import time
 import unicodedata
 
 from dateutil.tz import tzutc
@@ -1095,3 +1096,11 @@ def test_delimited_date():
         result = Timestamp("13-01-2000")
     expected = Timestamp(2000, 1, 13)
     assert result == expected
+
+
+def test_utctimetuple():
+    # GH 32174
+    ts = Timestamp("2000-01-01", tz="UTC")
+    result = ts.utctimetuple()
+    expected = time.struct_time((2000, 1, 1, 0, 0, 0, 5, 1, 0))
+    assert result == expected
diff --git a/pandas/tests/tseries/offsets/test_business_hour.py b/pandas/tests/tseries/offsets/test_business_hour.py
index 79e7a5ff67..319cc053d5 100644
--- a/pandas/tests/tseries/offsets/test_business_hour.py
+++ b/pandas/tests/tseries/offsets/test_business_hour.py
@@ -241,6 +241,12 @@ class TestBusinessHour:
 
         assert dt - offset2 == dt + _offset(-3)
 
+    def test_multiply_by_zero(self, dt, offset1, offset2):
+        assert dt - 0 * offset1 == dt
+        assert dt + 0 * offset1 == dt
+        assert dt - 0 * offset2 == dt
+        assert dt + 0 * offset2 == dt
+
     def testRollback1(
         self,
         dt,
@@ -972,6 +978,12 @@ class TestBusinessHour:
         for idx in [idx1, idx2, idx3]:
             tm.assert_index_equal(idx, expected)
 
+    def test_short_datetimeindex_creation(self):
+        # gh-49835
+        idx4 = date_range(start="2014-07-01 10:00", freq="BH", periods=1)
+        expected4 = DatetimeIndex(["2014-07-01 10:00"], freq="BH")
+        tm.assert_index_equal(idx4, expected4)
+
     def test_bday_ignores_timedeltas(self):
         idx = date_range("2010/02/01", "2010/02/10", freq="12H")
         t1 = idx + BDay(offset=Timedelta(3, unit="H"))