Repository: pandas-dev/pandas
Pull request #56084 (2023-11-20 18:54:44)

CI: Test clipboard on Linux via pyqt
closes https://github.com/pandas-dev/pandas/issues/52129


Patch:
diff --git a/ci/deps/actions-310.yaml b/ci/deps/actions-310.yaml
index f03ee2d321..7d08f2df5a 100644
--- a/ci/deps/actions-310.yaml
+++ b/ci/deps/actions-310.yaml
@@ -15,6 +15,7 @@ dependencies:
   - pytest-cov
   - pytest-xdist>=2.2.0
   - pytest-localserver>=0.7.1
+  - pytest-qt>=4.2.0
   - boto3
 
   # required dependencies
@@ -42,6 +43,7 @@ dependencies:
   - psycopg2>=2.9.6
   - pyarrow>=10.0.1
   - pymysql>=1.0.2
+  - pyqt>=5.15.9
   - pyreadstat>=1.2.0
   - pytables>=3.8.0
   - python-calamine>=0.1.6
@@ -58,5 +60,4 @@ dependencies:
   - pip:
     - adbc-driver-postgresql>=0.8.0
     - adbc-driver-sqlite>=0.8.0
-    - pyqt5>=5.15.8
     - tzdata>=2022.7
diff --git a/ci/deps/actions-311-downstream_compat.yaml b/ci/deps/actions-311-downstream_compat.yaml
index 7c1173a9c4..7a2bbe442c 100644
--- a/ci/deps/actions-311-downstream_compat.yaml
+++ b/ci/deps/actions-311-downstream_compat.yaml
@@ -16,6 +16,7 @@ dependencies:
   - pytest-cov
   - pytest-xdist>=2.2.0
   - pytest-localserver>=0.7.1
+  - pytest-qt>=4.2.0
   - boto3
 
   # required dependencies
@@ -43,6 +44,7 @@ dependencies:
   - psycopg2>=2.9.6
   - pyarrow>=10.0.1
   - pymysql>=1.0.2
+  - pyqt>=5.15.9
   - pyreadstat>=1.2.0
   - pytables>=3.8.0
   - python-calamine>=0.1.6
@@ -73,5 +75,4 @@ dependencies:
     - adbc-driver-postgresql>=0.8.0
     - adbc-driver-sqlite>=0.8.0
     - dataframe-api-compat>=0.1.7
-    - pyqt5>=5.15.8
     - tzdata>=2022.7
diff --git a/ci/deps/actions-311.yaml b/ci/deps/actions-311.yaml
index da73169513..681345a345 100644
--- a/ci/deps/actions-311.yaml
+++ b/ci/deps/actions-311.yaml
@@ -15,6 +15,7 @@ dependencies:
   - pytest-cov
   - pytest-xdist>=2.2.0
   - pytest-localserver>=0.7.1
+  - pytest-qt>=4.2.0
   - boto3
 
   # required dependencies
@@ -38,6 +39,7 @@ dependencies:
   - numexpr>=2.8.4
   - odfpy>=1.4.1
   - qtpy>=2.3.0
+  - pyqt>=5.15.9
   - openpyxl>=3.1.0
   - psycopg2>=2.9.6
   - pyarrow>=10.0.1
@@ -58,5 +60,4 @@ dependencies:
   - pip:
     - adbc-driver-postgresql>=0.8.0
     - adbc-driver-sqlite>=0.8.0
-    - pyqt5>=5.15.8
     - tzdata>=2022.7
diff --git a/ci/deps/actions-39-minimum_versions.yaml b/ci/deps/actions-39-minimum_versions.yaml
index 42729046c8..2e3b03f63e 100644
--- a/ci/deps/actions-39-minimum_versions.yaml
+++ b/ci/deps/actions-39-minimum_versions.yaml
@@ -17,6 +17,7 @@ dependencies:
   - pytest-cov
   - pytest-xdist>=2.2.0
   - pytest-localserver>=0.7.1
+  - pytest-qt>=4.2.0
   - boto3
 
   # required dependencies
@@ -44,6 +45,7 @@ dependencies:
   - psycopg2=2.9.6
   - pyarrow=10.0.1
   - pymysql=1.0.2
+  - pyqt=5.15.9
   - pyreadstat=1.2.0
   - pytables=3.8.0
   - python-calamine=0.1.6
@@ -61,5 +63,4 @@ dependencies:
     - adbc-driver-postgresql==0.8.0
     - adbc-driver-sqlite==0.8.0
     - dataframe-api-compat==0.1.7
-    - pyqt5==5.15.8
     - tzdata==2022.7
diff --git a/ci/deps/actions-39.yaml b/ci/deps/actions-39.yaml
index 85bac1da09..d3e545b636 100644
--- a/ci/deps/actions-39.yaml
+++ b/ci/deps/actions-39.yaml
@@ -15,6 +15,7 @@ dependencies:
   - pytest-cov
   - pytest-xdist>=2.2.0
   - pytest-localserver>=0.7.1
+  - pytest-qt>=4.2.0
   - boto3
 
   # required dependencies
@@ -42,6 +43,7 @@ dependencies:
   - psycopg2>=2.9.6
   - pyarrow>=10.0.1
   - pymysql>=1.0.2
+  - pyqt>=5.15.9
   - pyreadstat>=1.2.0
   - pytables>=3.8.0
   - python-calamine>=0.1.6
@@ -58,5 +60,4 @@ dependencies:
   - pip:
     - adbc-driver-postgresql>=0.8.0
     - adbc-driver-sqlite>=0.8.0
-    - pyqt5>=5.15.8
     - tzdata>=2022.7
diff --git a/ci/deps/circle-310-arm64.yaml b/ci/deps/circle-310-arm64.yaml
index 8f45666d3b..0098406037 100644
--- a/ci/deps/circle-310-arm64.yaml
+++ b/ci/deps/circle-310-arm64.yaml
@@ -15,6 +15,7 @@ dependencies:
   - pytest-cov
   - pytest-xdist>=2.2.0
   - pytest-localserver>=0.7.1
+  - pytest-qt>=4.2.0
   - boto3
 
   # required dependencies
@@ -42,6 +43,7 @@ dependencies:
   - psycopg2>=2.9.6
   - pyarrow>=10.0.1
   - pymysql>=1.0.2
+  - pyqt>=5.15.9
   - pyreadstat>=1.2.0
   - pytables>=3.8.0
   - python-calamine>=0.1.6
diff --git a/doc/source/getting_started/install.rst b/doc/source/getting_started/install.rst
index 27131dd113..c048c71613 100644
--- a/doc/source/getting_started/install.rst
+++ b/doc/source/getting_started/install.rst
@@ -397,7 +397,7 @@ Installable with ``pip install "pandas[clipboard]"``.
 ========================= ================== =============== =============================================================
 Dependency                Minimum Version    pip extra       Notes
 ========================= ================== =============== =============================================================
-PyQt4/PyQt5               5.15.8             clipboard       Clipboard I/O
+PyQt4/PyQt5               5.15.9             clipboard       Clipboard I/O
 qtpy                      2.3.0              clipboard       Clipboard I/O
 ========================= ================== =============== =============================================================
 
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 6b2c24c4e8..f155fb27d0 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -447,6 +447,7 @@ Datetimelike
 - Bug in :func:`testing.assert_extension_array_equal` that could use the wrong unit when comparing resolutions (:issue:`55730`)
 - Bug in :func:`to_datetime` and :class:`DatetimeIndex` when passing a list of mixed-string-and-numeric types incorrectly raising (:issue:`55780`)
 - Bug in :func:`to_datetime` and :class:`DatetimeIndex` when passing mixed-type objects with a mix of timezones or mix of timezone-awareness failing to raise ``ValueError`` (:issue:`55693`)
+- Bug in :meth:`DatetimeIndex.shift` with non-nanosecond resolution incorrectly returning with nanosecond resolution (:issue:`56117`)
 - Bug in :meth:`DatetimeIndex.union` returning object dtype for tz-aware indexes with the same timezone but different units (:issue:`55238`)
 - Bug in :meth:`Index.is_monotonic_increasing` and :meth:`Index.is_monotonic_decreasing` always caching :meth:`Index.is_unique` as ``True`` when first value in index is ``NaT`` (:issue:`55755`)
 - Bug in :meth:`Index.view` to a datetime64 dtype with non-supported resolution incorrectly raising (:issue:`55710`)
diff --git a/environment.yml b/environment.yml
index 7558b8cf59..4aa8073e93 100644
--- a/environment.yml
+++ b/environment.yml
@@ -16,6 +16,8 @@ dependencies:
   - pytest>=7.3.2
   - pytest-cov
   - pytest-xdist>=2.2.0
+  - pytest-qt>=4.2.0
+  - pyqt>=5.15.9
   - coverage
 
   # required dependencies
diff --git a/pandas/compat/_optional.py b/pandas/compat/_optional.py
index 84cf7af0fe..1354c0a6db 100644
--- a/pandas/compat/_optional.py
+++ b/pandas/compat/_optional.py
@@ -52,7 +52,7 @@ VERSIONS = {
     "zstandard": "0.19.0",
     "tzdata": "2022.7",
     "qtpy": "2.3.0",
-    "pyqt5": "5.15.8",
+    "pyqt5": "5.15.9",
 }
 
 # A mapping from import name to package name (on PyPI) for packages where
diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py
index a3e6c50b21..264ca8aa11 100644
--- a/pandas/core/indexes/datetimelike.py
+++ b/pandas/core/indexes/datetimelike.py
@@ -518,7 +518,7 @@ class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, ABC):
         #  appropriate timezone from `start` and `end`, so tz does not need
         #  to be passed explicitly.
         result = self._data._generate_range(
-            start=start, end=end, periods=None, freq=self.freq
+            start=start, end=end, periods=None, freq=self.freq, unit=self.unit
         )
         return type(self)._simple_new(result, name=self.name)
 
diff --git a/pyproject.toml b/pyproject.toml
index c9eb8d9ac4..65c647e439 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -83,7 +83,7 @@ html = ['beautifulsoup4>=4.11.2', 'html5lib>=1.1', 'lxml>=4.9.2']
 xml = ['lxml>=4.9.2']
 plot = ['matplotlib>=3.6.3']
 output-formatting = ['jinja2>=3.1.2', 'tabulate>=0.9.0']
-clipboard = ['PyQt5>=5.15.8', 'qtpy>=2.3.0']
+clipboard = ['PyQt5>=5.15.9', 'qtpy>=2.3.0']
 compression = ['zstandard>=0.19.0']
 consortium-standard = ['dataframe-api-compat>=0.1.7']
 all = ['adbc-driver-postgresql>=0.8.0',
@@ -109,7 +109,7 @@ all = ['adbc-driver-postgresql>=0.8.0',
        'psycopg2>=2.9.6',
        'pyarrow>=10.0.1',
        'pymysql>=1.0.2',
-       'PyQt5>=5.15.8',
+       'PyQt5>=5.15.9',
        'pyreadstat>=1.2.0',
        'pytest>=7.3.2',
        'pytest-xdist>=2.2.0',
diff --git a/requirements-dev.txt b/requirements-dev.txt
index d074949b22..fa3ef1c214 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -9,6 +9,8 @@ meson-python==0.13.1
 pytest>=7.3.2
 pytest-cov
 pytest-xdist>=2.2.0
+pytest-qt>=4.2.0
+PyQt5>=5.15.9
 coverage
 python-dateutil
 numpy<2
diff --git a/scripts/generate_pip_deps_from_conda.py b/scripts/generate_pip_deps_from_conda.py
index 49fecca253..5fcf09cd07 100755
--- a/scripts/generate_pip_deps_from_conda.py
+++ b/scripts/generate_pip_deps_from_conda.py
@@ -25,12 +25,13 @@ import yaml
 
 EXCLUDE = {"python", "c-compiler", "cxx-compiler"}
 REMAP_VERSION = {"tzdata": "2022.7"}
-RENAME = {
+CONDA_TO_PIP = {
     "pytables": "tables",
     "psycopg2": "psycopg2-binary",
     "dask-core": "dask",
     "seaborn-base": "seaborn",
     "sqlalchemy": "SQLAlchemy",
+    "pyqt": "PyQt5",
 }
 
 
@@ -40,7 +41,7 @@ def conda_package_to_pip(package: str):
 
     In most cases they are the same, those are the exceptions:
     - Packages that should be excluded (in `EXCLUDE`)
-    - Packages that should be renamed (in `RENAME`)
+    - Packages that should be renamed (in `CONDA_TO_PIP`)
     - A package requiring a specific version, in conda is defined with a single
       equal (e.g. ``pandas=1.0``) and in pip with two (e.g. ``pandas==1.0``)
     """
@@ -53,14 +54,14 @@ def conda_package_to_pip(package: str):
                 return
             if pkg in REMAP_VERSION:
                 return "".join((pkg, compare, REMAP_VERSION[pkg]))
-            if pkg in RENAME:
-                return "".join((RENAME[pkg], compare, version))
+            if pkg in CONDA_TO_PIP:
+                return "".join((CONDA_TO_PIP[pkg], compare, version))
 
     if package in EXCLUDE:
         return
 
-    if package in RENAME:
-        return RENAME[package]
+    if package in CONDA_TO_PIP:
+        return CONDA_TO_PIP[package]
 
     return package
 
diff --git a/scripts/validate_min_versions_in_sync.py b/scripts/validate_min_versions_in_sync.py
index d5c491f6eb..7dd3e96e6e 100755
--- a/scripts/validate_min_versions_in_sync.py
+++ b/scripts/validate_min_versions_in_sync.py
@@ -26,7 +26,7 @@ else:
 
 from typing import Any
 
-from scripts.generate_pip_deps_from_conda import RENAME
+from scripts.generate_pip_deps_from_conda import CONDA_TO_PIP
 
 DOC_PATH = pathlib.Path("doc/source/getting_started/install.rst").resolve()
 CI_PATH = next(
@@ -36,7 +36,7 @@ CODE_PATH = pathlib.Path("pandas/compat/_optional.py").resolve()
 SETUP_PATH = pathlib.Path("pyproject.toml").resolve()
 YAML_PATH = pathlib.Path("ci/deps")
 ENV_PATH = pathlib.Path("environment.yml")
-EXCLUDE_DEPS = {"tzdata", "blosc", "pandas-gbq"}
+EXCLUDE_DEPS = {"tzdata", "blosc", "pandas-gbq", "pyqt", "pyqt5"}
 EXCLUSION_LIST = frozenset(["python=3.8[build=*_pypy]"])
 # pandas package is not available
 # in pre-commit environment
@@ -169,8 +169,8 @@ def pin_min_versions_to_yaml_file(
         old_dep = yaml_package
         if yaml_versions is not None:
             old_dep = old_dep + ", ".join(yaml_versions)
-        if RENAME.get(yaml_package, yaml_package) in toml_map:
-            min_dep = toml_map[RENAME.get(yaml_package, yaml_package)]
+        if CONDA_TO_PIP.get(yaml_package, yaml_package) in toml_map:
+            min_dep = toml_map[CONDA_TO_PIP.get(yaml_package, yaml_package)]
         elif yaml_package in toml_map:
             min_dep = toml_map[yaml_package]
         else:


Test patch:
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index e156b1e0ae..33b6e7a8c2 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -88,7 +88,6 @@ jobs:
     name: ${{ matrix.name || format('ubuntu-latest {0}', matrix.env_file) }}
     env:
       PATTERN: ${{ matrix.pattern }}
-      EXTRA_APT: ${{ matrix.extra_apt || '' }}
       LANG: ${{ matrix.lang || 'C.UTF-8' }}
       LC_ALL: ${{ matrix.lc_all || '' }}
       PANDAS_COPY_ON_WRITE: ${{ matrix.pandas_copy_on_write || '0' }}
@@ -96,6 +95,8 @@ jobs:
       TEST_ARGS: ${{ matrix.test_args || '' }}
       PYTEST_WORKERS: 'auto'
       PYTEST_TARGET: ${{ matrix.pytest_target || 'pandas' }}
+      # Clipboard tests
+      QT_QPA_PLATFORM: offscreen
     concurrency:
       # https://github.community/t/concurrecy-not-work-for-push/183068/7
       group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-${{ matrix.env_file }}-${{ matrix.pattern }}-${{ matrix.extra_apt || '' }}-${{ matrix.pandas_copy_on_write || '' }}
@@ -145,8 +146,8 @@ jobs:
         fetch-depth: 0
 
     - name: Extra installs
-      # xsel for clipboard tests
-      run: sudo apt-get update && sudo apt-get install -y xsel ${{ env.EXTRA_APT }}
+      run: sudo apt-get update && sudo apt-get install -y ${{ matrix.extra_apt }}
+      if: ${{ matrix.extra_apt }}
 
     - name: Generate extra locales
       # These extra locales will be available for locale.setlocale() calls in tests
diff --git a/pandas/tests/indexes/datetimes/methods/test_shift.py b/pandas/tests/indexes/datetimes/methods/test_shift.py
index c50f75894d..d8bdcc2a17 100644
--- a/pandas/tests/indexes/datetimes/methods/test_shift.py
+++ b/pandas/tests/indexes/datetimes/methods/test_shift.py
@@ -20,10 +20,10 @@ class TestDatetimeIndexShift:
     # -------------------------------------------------------------
     # DatetimeIndex.shift is used in integer addition
 
-    def test_dti_shift_tzaware(self, tz_naive_fixture):
+    def test_dti_shift_tzaware(self, tz_naive_fixture, unit):
         # GH#9903
         tz = tz_naive_fixture
-        idx = DatetimeIndex([], name="xxx", tz=tz)
+        idx = DatetimeIndex([], name="xxx", tz=tz).as_unit(unit)
         tm.assert_index_equal(idx.shift(0, freq="h"), idx)
         tm.assert_index_equal(idx.shift(3, freq="h"), idx)
 
@@ -32,30 +32,31 @@ class TestDatetimeIndexShift:
             name="xxx",
             tz=tz,
             freq="h",
-        )
+        ).as_unit(unit)
         tm.assert_index_equal(idx.shift(0, freq="h"), idx)
         exp = DatetimeIndex(
             ["2011-01-01 13:00", "2011-01-01 14:00", "2011-01-01 15:00"],
             name="xxx",
             tz=tz,
             freq="h",
-        )
+        ).as_unit(unit)
         tm.assert_index_equal(idx.shift(3, freq="h"), exp)
         exp = DatetimeIndex(
             ["2011-01-01 07:00", "2011-01-01 08:00", "2011-01-01 09:00"],
             name="xxx",
             tz=tz,
             freq="h",
-        )
+        ).as_unit(unit)
         tm.assert_index_equal(idx.shift(-3, freq="h"), exp)
 
-    def test_dti_shift_freqs(self):
+    def test_dti_shift_freqs(self, unit):
         # test shift for DatetimeIndex and non DatetimeIndex
         # GH#8083
-        drange = date_range("20130101", periods=5)
+        drange = date_range("20130101", periods=5, unit=unit)
         result = drange.shift(1)
         expected = DatetimeIndex(
             ["2013-01-02", "2013-01-03", "2013-01-04", "2013-01-05", "2013-01-06"],
+            dtype=f"M8[{unit}]",
             freq="D",
         )
         tm.assert_index_equal(result, expected)
@@ -63,6 +64,7 @@ class TestDatetimeIndexShift:
         result = drange.shift(-1)
         expected = DatetimeIndex(
             ["2012-12-31", "2013-01-01", "2013-01-02", "2013-01-03", "2013-01-04"],
+            dtype=f"M8[{unit}]",
             freq="D",
         )
         tm.assert_index_equal(result, expected)
@@ -70,12 +72,13 @@ class TestDatetimeIndexShift:
         result = drange.shift(3, freq="2D")
         expected = DatetimeIndex(
             ["2013-01-07", "2013-01-08", "2013-01-09", "2013-01-10", "2013-01-11"],
+            dtype=f"M8[{unit}]",
             freq="D",
         )
         tm.assert_index_equal(result, expected)
 
-    def test_dti_shift_int(self):
-        rng = date_range("1/1/2000", periods=20)
+    def test_dti_shift_int(self, unit):
+        rng = date_range("1/1/2000", periods=20, unit=unit)
 
         result = rng + 5 * rng.freq
         expected = rng.shift(5)
@@ -85,25 +88,27 @@ class TestDatetimeIndexShift:
         expected = rng.shift(-5)
         tm.assert_index_equal(result, expected)
 
-    def test_dti_shift_no_freq(self):
+    def test_dti_shift_no_freq(self, unit):
         # GH#19147
-        dti = DatetimeIndex(["2011-01-01 10:00", "2011-01-01"], freq=None)
+        dti = DatetimeIndex(["2011-01-01 10:00", "2011-01-01"], freq=None).as_unit(unit)
         with pytest.raises(NullFrequencyError, match="Cannot shift with no freq"):
             dti.shift(2)
 
     @pytest.mark.parametrize("tzstr", ["US/Eastern", "dateutil/US/Eastern"])
-    def test_dti_shift_localized(self, tzstr):
-        dr = date_range("2011/1/1", "2012/1/1", freq="W-FRI")
+    def test_dti_shift_localized(self, tzstr, unit):
+        dr = date_range("2011/1/1", "2012/1/1", freq="W-FRI", unit=unit)
         dr_tz = dr.tz_localize(tzstr)
 
         result = dr_tz.shift(1, "10min")
         assert result.tz == dr_tz.tz
 
-    def test_dti_shift_across_dst(self):
+    def test_dti_shift_across_dst(self, unit):
         # GH 8616
-        idx = date_range("2013-11-03", tz="America/Chicago", periods=7, freq="h")
-        s = Series(index=idx[:-1], dtype=object)
-        result = s.shift(freq="h")
+        idx = date_range(
+            "2013-11-03", tz="America/Chicago", periods=7, freq="h", unit=unit
+        )
+        ser = Series(index=idx[:-1], dtype=object)
+        result = ser.shift(freq="h")
         expected = Series(index=idx[1:], dtype=object)
         tm.assert_series_equal(result, expected)
 
@@ -115,24 +120,26 @@ class TestDatetimeIndexShift:
             [1, "2014-11-14 01:00:00"],
         ],
     )
-    def test_dti_shift_near_midnight(self, shift, result_time):
+    def test_dti_shift_near_midnight(self, shift, result_time, unit):
         # GH 8616
         dt = datetime(2014, 11, 14, 0)
         dt_est = pytz.timezone("EST").localize(dt)
-        s = Series(data=[1], index=[dt_est])
-        result = s.shift(shift, freq="h")
-        expected = Series(1, index=DatetimeIndex([result_time], tz="EST"))
+        idx = DatetimeIndex([dt_est]).as_unit(unit)
+        ser = Series(data=[1], index=idx)
+        result = ser.shift(shift, freq="h")
+        exp_index = DatetimeIndex([result_time], tz="EST").as_unit(unit)
+        expected = Series(1, index=exp_index)
         tm.assert_series_equal(result, expected)
 
-    def test_shift_periods(self):
+    def test_shift_periods(self, unit):
         # GH#22458 : argument 'n' was deprecated in favor of 'periods'
-        idx = date_range(start=START, end=END, periods=3)
+        idx = date_range(start=START, end=END, periods=3, unit=unit)
         tm.assert_index_equal(idx.shift(periods=0), idx)
         tm.assert_index_equal(idx.shift(0), idx)
 
     @pytest.mark.parametrize("freq", ["B", "C"])
-    def test_shift_bday(self, freq):
-        rng = date_range(START, END, freq=freq)
+    def test_shift_bday(self, freq, unit):
+        rng = date_range(START, END, freq=freq, unit=unit)
         shifted = rng.shift(5)
         assert shifted[0] == rng[5]
         assert shifted.freq == rng.freq
@@ -145,18 +152,18 @@ class TestDatetimeIndexShift:
         assert shifted[0] == rng[0]
         assert shifted.freq == rng.freq
 
-    def test_shift_bmonth(self):
-        rng = date_range(START, END, freq=pd.offsets.BMonthEnd())
+    def test_shift_bmonth(self, unit):
+        rng = date_range(START, END, freq=pd.offsets.BMonthEnd(), unit=unit)
         shifted = rng.shift(1, freq=pd.offsets.BDay())
         assert shifted[0] == rng[0] + pd.offsets.BDay()
 
-        rng = date_range(START, END, freq=pd.offsets.BMonthEnd())
+        rng = date_range(START, END, freq=pd.offsets.BMonthEnd(), unit=unit)
         with tm.assert_produces_warning(pd.errors.PerformanceWarning):
             shifted = rng.shift(1, freq=pd.offsets.CDay())
             assert shifted[0] == rng[0] + pd.offsets.CDay()
 
-    def test_shift_empty(self):
+    def test_shift_empty(self, unit):
         # GH#14811
-        dti = date_range(start="2016-10-21", end="2016-10-21", freq="BME")
+        dti = date_range(start="2016-10-21", end="2016-10-21", freq="BME", unit=unit)
         result = dti.shift(1)
         tm.assert_index_equal(result, dti)
diff --git a/pandas/tests/io/pytables/test_put.py b/pandas/tests/io/pytables/test_put.py
index 24ee0d28a1..c109b72e9c 100644
--- a/pandas/tests/io/pytables/test_put.py
+++ b/pandas/tests/io/pytables/test_put.py
@@ -1,4 +1,3 @@
-import datetime
 import re
 
 import numpy as np
@@ -15,6 +14,7 @@ from pandas import (
     Series,
     _testing as tm,
     concat,
+    date_range,
 )
 from pandas.tests.io.pytables.common import (
     _maybe_remove,
@@ -279,15 +279,9 @@ def test_store_multiindex(setup_path):
     with ensure_clean_store(setup_path) as store:
 
         def make_index(names=None):
-            return MultiIndex.from_tuples(
-                [
-                    (datetime.datetime(2013, 12, d), s, t)
-                    for d in range(1, 3)
-                    for s in range(2)
-                    for t in range(3)
-                ],
-                names=names,
-            )
+            dti = date_range("2013-12-01", "2013-12-02")
+            mi = MultiIndex.from_product([dti, range(2), range(3)], names=names)
+            return mi
 
         # no names
         _maybe_remove(store, "df")
@@ -306,11 +300,11 @@ def test_store_multiindex(setup_path):
         tm.assert_frame_equal(store.select("df"), df)
 
         # series
-        _maybe_remove(store, "s")
-        s = Series(np.zeros(12), index=make_index(["date", None, None]))
-        store.append("s", s)
+        _maybe_remove(store, "ser")
+        ser = Series(np.zeros(12), index=make_index(["date", None, None]))
+        store.append("ser", ser)
         xp = Series(np.zeros(12), index=make_index(["date", "level_1", "level_2"]))
-        tm.assert_series_equal(store.select("s"), xp)
+        tm.assert_series_equal(store.select("ser"), xp)
 
         # dup with column
         _maybe_remove(store, "df")
diff --git a/pandas/tests/io/pytables/test_read.py b/pandas/tests/io/pytables/test_read.py
index 9b4590ca3f..32af61de05 100644
--- a/pandas/tests/io/pytables/test_read.py
+++ b/pandas/tests/io/pytables/test_read.py
@@ -154,7 +154,7 @@ def test_pytables_native_read(datapath):
         datapath("io", "data", "legacy_hdf/pytables_native.h5"), mode="r"
     ) as store:
         d2 = store["detector/readout"]
-        assert isinstance(d2, DataFrame)
+    assert isinstance(d2, DataFrame)
 
 
 @pytest.mark.skipif(is_platform_windows(), reason="native2 read fails oddly on windows")
@@ -164,7 +164,7 @@ def test_pytables_native2_read(datapath):
     ) as store:
         str(store)
         d1 = store["detector"]
-        assert isinstance(d1, DataFrame)
+    assert isinstance(d1, DataFrame)
 
 
 def test_legacy_table_fixed_format_read_py2(datapath):
@@ -174,28 +174,29 @@ def test_legacy_table_fixed_format_read_py2(datapath):
         datapath("io", "data", "legacy_hdf", "legacy_table_fixed_py2.h5"), mode="r"
     ) as store:
         result = store.select("df")
-        expected = DataFrame(
-            [[1, 2, 3, "D"]],
-            columns=["A", "B", "C", "D"],
-            index=Index(["ABC"], name="INDEX_NAME"),
-        )
-        tm.assert_frame_equal(expected, result)
+    expected = DataFrame(
+        [[1, 2, 3, "D"]],
+        columns=["A", "B", "C", "D"],
+        index=Index(["ABC"], name="INDEX_NAME"),
+    )
+    tm.assert_frame_equal(expected, result)
 
 
 def test_legacy_table_fixed_format_read_datetime_py2(datapath):
     # GH 31750
     # legacy table with fixed format and datetime64 column written in Python 2
+    expected = DataFrame(
+        [[Timestamp("2020-02-06T18:00")]],
+        columns=["A"],
+        index=Index(["date"]),
+        dtype="M8[ns]",
+    )
     with ensure_clean_store(
         datapath("io", "data", "legacy_hdf", "legacy_table_fixed_datetime_py2.h5"),
         mode="r",
     ) as store:
         result = store.select("df")
-        expected = DataFrame(
-            [[Timestamp("2020-02-06T18:00")]],
-            columns=["A"],
-            index=Index(["date"]),
-        )
-        tm.assert_frame_equal(expected, result)
+    tm.assert_frame_equal(expected, result)
 
 
 def test_legacy_table_read_py2(datapath):
@@ -264,7 +265,7 @@ def test_read_hdf_iterator(tmp_path, setup_path):
     with closing(iterator.store):
         assert isinstance(iterator, TableIterator)
         indirect = next(iterator.__iter__())
-        tm.assert_frame_equal(direct, indirect)
+    tm.assert_frame_equal(direct, indirect)
 
 
 def test_read_nokey(tmp_path, setup_path):
@@ -387,7 +388,7 @@ def test_read_py2_hdf_file_in_py3(datapath):
         mode="r",
     ) as store:
         result = store["p"]
-        tm.assert_frame_equal(result, expected)
+    tm.assert_frame_equal(result, expected)
 
 
 def test_read_infer_string(tmp_path, setup_path):
diff --git a/pandas/tests/io/pytables/test_retain_attributes.py b/pandas/tests/io/pytables/test_retain_attributes.py
index aef6fc0460..6284b826c3 100644
--- a/pandas/tests/io/pytables/test_retain_attributes.py
+++ b/pandas/tests/io/pytables/test_retain_attributes.py
@@ -1,9 +1,8 @@
 import pytest
 
-from pandas._libs.tslibs import Timestamp
-
 from pandas import (
     DataFrame,
+    DatetimeIndex,
     Series,
     _testing as tm,
     date_range,
@@ -18,11 +17,10 @@ from pandas.tests.io.pytables.common import (
 pytestmark = pytest.mark.single_cpu
 
 
-def test_retain_index_attributes(setup_path):
+def test_retain_index_attributes(setup_path, unit):
     # GH 3499, losing frequency info on index recreation
-    df = DataFrame(
-        {"A": Series(range(3), index=date_range("2000-1-1", periods=3, freq="h"))}
-    )
+    dti = date_range("2000-1-1", periods=3, freq="h", unit=unit)
+    df = DataFrame({"A": Series(range(3), index=dti)})
 
     with ensure_clean_store(setup_path) as store:
         _maybe_remove(store, "data")
@@ -37,37 +35,30 @@ def test_retain_index_attributes(setup_path):
                     getattr(result, idx), attr, None
                 )
 
+        dti2 = date_range("2002-1-1", periods=3, freq="D", unit=unit)
         # try to append a table with a different frequency
         with tm.assert_produces_warning(errors.AttributeConflictWarning):
-            df2 = DataFrame(
-                {
-                    "A": Series(
-                        range(3), index=date_range("2002-1-1", periods=3, freq="D")
-                    )
-                }
-            )
+            df2 = DataFrame({"A": Series(range(3), index=dti2)})
             store.append("data", df2)
 
         assert store.get_storer("data").info["index"]["freq"] is None
 
         # this is ok
         _maybe_remove(store, "df2")
+        dti3 = DatetimeIndex(
+            ["2001-01-01", "2001-01-02", "2002-01-01"], dtype=f"M8[{unit}]"
+        )
         df2 = DataFrame(
             {
                 "A": Series(
                     range(3),
-                    index=[
-                        Timestamp("20010101"),
-                        Timestamp("20010102"),
-                        Timestamp("20020101"),
-                    ],
+                    index=dti3,
                 )
             }
         )
         store.append("df2", df2)
-        df3 = DataFrame(
-            {"A": Series(range(3), index=date_range("2002-1-1", periods=3, freq="D"))}
-        )
+        dti4 = date_range("2002-1-1", periods=3, freq="D", unit=unit)
+        df3 = DataFrame({"A": Series(range(3), index=dti4)})
         store.append("df2", df3)
 
 
diff --git a/pandas/tests/io/pytables/test_round_trip.py b/pandas/tests/io/pytables/test_round_trip.py
index af24a5cf7e..4f908f28cb 100644
--- a/pandas/tests/io/pytables/test_round_trip.py
+++ b/pandas/tests/io/pytables/test_round_trip.py
@@ -10,6 +10,7 @@ from pandas.compat import is_platform_windows
 import pandas as pd
 from pandas import (
     DataFrame,
+    DatetimeIndex,
     Index,
     Series,
     _testing as tm,
@@ -320,7 +321,11 @@ def test_index_types(setup_path):
     ser = Series(values, [1, 5])
     _check_roundtrip(ser, func, path=setup_path)
 
-    ser = Series(values, [datetime.datetime(2012, 1, 1), datetime.datetime(2012, 1, 2)])
+    dti = DatetimeIndex(["2012-01-01", "2012-01-02"], dtype="M8[ns]")
+    ser = Series(values, index=dti)
+    _check_roundtrip(ser, func, path=setup_path)
+
+    ser.index = ser.index.as_unit("s")
     _check_roundtrip(ser, func, path=setup_path)
 
 
diff --git a/pandas/tests/io/pytables/test_store.py b/pandas/tests/io/pytables/test_store.py
index b61b6f0772..4624a48df1 100644
--- a/pandas/tests/io/pytables/test_store.py
+++ b/pandas/tests/io/pytables/test_store.py
@@ -542,17 +542,16 @@ def test_store_index_name(setup_path):
 
 
 @pytest.mark.parametrize("tz", [None, "US/Pacific"])
-@pytest.mark.parametrize("unit", ["s", "ms", "us", "ns"])
 @pytest.mark.parametrize("table_format", ["table", "fixed"])
 def test_store_index_name_numpy_str(tmp_path, table_format, setup_path, unit, tz):
     # GH #13492
-    idx = Index(
-        pd.to_datetime([dt.date(2000, 1, 1), dt.date(2000, 1, 2)]),
+    idx = DatetimeIndex(
+        [dt.date(2000, 1, 1), dt.date(2000, 1, 2)],
         name="cols\u05d2",
     ).tz_localize(tz)
     idx1 = (
-        Index(
-            pd.to_datetime([dt.date(2010, 1, 1), dt.date(2010, 1, 2)]),
+        DatetimeIndex(
+            [dt.date(2010, 1, 1), dt.date(2010, 1, 2)],
             name="rows\u05d0",
         )
         .as_unit(unit)
diff --git a/pandas/tests/io/pytables/test_time_series.py b/pandas/tests/io/pytables/test_time_series.py
index 26492f72f1..cfe25f03e1 100644
--- a/pandas/tests/io/pytables/test_time_series.py
+++ b/pandas/tests/io/pytables/test_time_series.py
@@ -5,6 +5,7 @@ import pytest
 
 from pandas import (
     DataFrame,
+    DatetimeIndex,
     Series,
     _testing as tm,
 )
@@ -13,10 +14,12 @@ from pandas.tests.io.pytables.common import ensure_clean_store
 pytestmark = pytest.mark.single_cpu
 
 
-def test_store_datetime_fractional_secs(setup_path):
+@pytest.mark.parametrize("unit", ["us", "ns"])
+def test_store_datetime_fractional_secs(setup_path, unit):
+    dt = datetime.datetime(2012, 1, 2, 3, 4, 5, 123456)
+    dti = DatetimeIndex([dt], dtype=f"M8[{unit}]")
+    series = Series([0], index=dti)
     with ensure_clean_store(setup_path) as store:
-        dt = datetime.datetime(2012, 1, 2, 3, 4, 5, 123456)
-        series = Series([0], [dt])
         store["a"] = series
         assert store["a"].index[0] == dt
 
diff --git a/pandas/tests/io/pytables/test_timezones.py b/pandas/tests/io/pytables/test_timezones.py
index 676b937451..8c61830ebe 100644
--- a/pandas/tests/io/pytables/test_timezones.py
+++ b/pandas/tests/io/pytables/test_timezones.py
@@ -148,16 +148,20 @@ def test_append_with_timezones_as_index(setup_path, gettz):
         tm.assert_frame_equal(result, df)
 
 
-def test_roundtrip_tz_aware_index(setup_path):
+def test_roundtrip_tz_aware_index(setup_path, unit):
     # GH 17618
-    time = Timestamp("2000-01-01 01:00:00", tz="US/Eastern")
-    df = DataFrame(data=[0], index=[time])
+    ts = Timestamp("2000-01-01 01:00:00", tz="US/Eastern")
+    dti = DatetimeIndex([ts]).as_unit(unit)
+    df = DataFrame(data=[0], index=dti)
 
     with ensure_clean_store(setup_path) as store:
         store.put("frame", df, format="fixed")
         recons = store["frame"]
         tm.assert_frame_equal(recons, df)
-        assert recons.index[0]._value == 946706400000000000
+
+    value = recons.index[0]._value
+    denom = {"ns": 1, "us": 1000, "ms": 10**6, "s": 10**9}[unit]
+    assert value == 946706400000000000 // denom
 
 
 def test_store_index_name_with_tz(setup_path):
@@ -365,7 +369,7 @@ def test_py2_created_with_datetimez(datapath):
     # Python 3.
     #
     # GH26443
-    index = [Timestamp("2019-01-01T18:00").tz_localize("America/New_York")]
+    index = DatetimeIndex(["2019-01-01T18:00"], dtype="M8[ns, America/New_York]")
     expected = DataFrame({"data": 123}, index=index)
     with ensure_clean_store(
         datapath("io", "data", "legacy_hdf", "gh26443.h5"), mode="r"
diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py
index 10e0467c5d..3d150aaa28 100644
--- a/pandas/tests/io/test_clipboard.py
+++ b/pandas/tests/io/test_clipboard.py
@@ -1,13 +1,8 @@
-import os
 from textwrap import dedent
 
 import numpy as np
 import pytest
 
-from pandas.compat import (
-    is_ci_environment,
-    is_platform_mac,
-)
 from pandas.errors import (
     PyperclipException,
     PyperclipWindowsException,
@@ -30,8 +25,7 @@ from pandas.core.arrays import (
 from pandas.io.clipboard import (
     CheckedCall,
     _stringifyText,
-    clipboard_get,
-    clipboard_set,
+    init_qt_clipboard,
 )
 
 
@@ -205,60 +199,34 @@ def test_stringify_text(text):
 
 
 @pytest.fixture
-def mock_clipboard(monkeypatch, request):
-    """Fixture mocking clipboard IO.
-
-    This mocks pandas.io.clipboard.clipboard_get and
-    pandas.io.clipboard.clipboard_set.
-
-    This uses a local dict for storing data. The dictionary
-    key used is the test ID, available with ``request.node.name``.
-
-    This returns the local dictionary, for direct manipulation by
-    tests.
-    """
-    # our local clipboard for tests
-    _mock_data = {}
-
-    def _mock_set(data):
-        _mock_data[request.node.name] = data
-
-    def _mock_get():
-        return _mock_data[request.node.name]
-
-    monkeypatch.setattr("pandas.io.clipboard.clipboard_set", _mock_set)
-    monkeypatch.setattr("pandas.io.clipboard.clipboard_get", _mock_get)
-
-    yield _mock_data
-
+def set_pyqt_clipboard(monkeypatch):
+    qt_cut, qt_paste = init_qt_clipboard()
+    with monkeypatch.context() as m:
+        m.setattr(pd.io.clipboard, "clipboard_set", qt_cut)
+        m.setattr(pd.io.clipboard, "clipboard_get", qt_paste)
+        yield
 
-@pytest.mark.clipboard
-def test_mock_clipboard(mock_clipboard):
-    import pandas.io.clipboard
 
-    pandas.io.clipboard.clipboard_set("abc")
-    assert "abc" in set(mock_clipboard.values())
-    result = pandas.io.clipboard.clipboard_get()
-    assert result == "abc"
+@pytest.fixture
+def clipboard(qapp):
+    clip = qapp.clipboard()
+    yield clip
+    clip.clear()
 
 
 @pytest.mark.single_cpu
 @pytest.mark.clipboard
-@pytest.mark.usefixtures("mock_clipboard")
+@pytest.mark.usefixtures("set_pyqt_clipboard")
+@pytest.mark.usefixtures("clipboard")
 class TestClipboard:
-    def check_round_trip_frame(self, data, excel=None, sep=None, encoding=None):
-        data.to_clipboard(excel=excel, sep=sep, encoding=encoding)
-        result = read_clipboard(sep=sep or "\t", index_col=0, encoding=encoding)
-        tm.assert_frame_equal(data, result)
-
     # Test that default arguments copy as tab delimited
-    def test_round_trip_frame(self, df):
-        self.check_round_trip_frame(df)
-
     # Test that explicit delimiters are respected
-    @pytest.mark.parametrize("sep", ["\t", ",", "|"])
-    def test_round_trip_frame_sep(self, df, sep):
-        self.check_round_trip_frame(df, sep=sep)
+    @pytest.mark.parametrize("sep", [None, "\t", ",", "|"])
+    @pytest.mark.parametrize("encoding", [None, "UTF-8", "utf-8", "utf8"])
+    def test_round_trip_frame_sep(self, df, sep, encoding):
+        df.to_clipboard(excel=None, sep=sep, encoding=encoding)
+        result = read_clipboard(sep=sep or "\t", index_col=0, encoding=encoding)
+        tm.assert_frame_equal(df, result)
 
     # Test white space separator
     def test_round_trip_frame_string(self, df):
@@ -286,22 +254,21 @@ class TestClipboard:
     # delimited and excel="True"
     @pytest.mark.parametrize("sep", ["\t", None, "default"])
     @pytest.mark.parametrize("excel", [True, None, "default"])
-    def test_clipboard_copy_tabs_default(self, sep, excel, df, request, mock_clipboard):
+    def test_clipboard_copy_tabs_default(self, sep, excel, df, clipboard):
         kwargs = build_kwargs(sep, excel)
         df.to_clipboard(**kwargs)
-        assert mock_clipboard[request.node.name] == df.to_csv(sep="\t")
+        assert clipboard.text() == df.to_csv(sep="\t")
 
     # Tests reading of white space separated tables
     @pytest.mark.parametrize("sep", [None, "default"])
-    @pytest.mark.parametrize("excel", [False])
-    def test_clipboard_copy_strings(self, sep, excel, df):
-        kwargs = build_kwargs(sep, excel)
+    def test_clipboard_copy_strings(self, sep, df):
+        kwargs = build_kwargs(sep, False)
         df.to_clipboard(**kwargs)
         result = read_clipboard(sep=r"\s+")
         assert result.to_string() == df.to_string()
         assert df.shape == result.shape
 
-    def test_read_clipboard_infer_excel(self, request, mock_clipboard):
+    def test_read_clipboard_infer_excel(self, clipboard):
         # gh-19010: avoid warnings
         clip_kwargs = {"engine": "python"}
 
@@ -312,7 +279,7 @@ class TestClipboard:
             4\tHarry Carney
             """.strip()
         )
-        mock_clipboard[request.node.name] = text
+        clipboard.setText(text)
         df = read_clipboard(**clip_kwargs)
 
         # excel data is parsed correctly
@@ -326,7 +293,7 @@ class TestClipboard:
             3  4
             """.strip()
         )
-        mock_clipboard[request.node.name] = text
+        clipboard.setText(text)
         res = read_clipboard(**clip_kwargs)
 
         text = dedent(
@@ -336,16 +303,16 @@ class TestClipboard:
             3  4
             """.strip()
         )
-        mock_clipboard[request.node.name] = text
+        clipboard.setText(text)
         exp = read_clipboard(**clip_kwargs)
 
         tm.assert_frame_equal(res, exp)
 
-    def test_infer_excel_with_nulls(self, request, mock_clipboard):
+    def test_infer_excel_with_nulls(self, clipboard):
         # GH41108
         text = "col1\tcol2\n1\tred\n\tblue\n2\tgreen"
 
-        mock_clipboard[request.node.name] = text
+        clipboard.setText(text)
         df = read_clipboard()
         df_expected = DataFrame(
             data={"col1": [1, None, 2], "col2": ["red", "blue", "green"]}
@@ -376,10 +343,10 @@ class TestClipboard:
             ),
         ],
     )
-    def test_infer_excel_with_multiindex(self, request, mock_clipboard, multiindex):
+    def test_infer_excel_with_multiindex(self, clipboard, multiindex):
         # GH41108
 
-        mock_clipboard[request.node.name] = multiindex[0]
+        clipboard.setText(multiindex[0])
         df = read_clipboard()
         df_expected = DataFrame(
             data={"col1": [1, None, 2], "col2": ["red", "blue", "green"]},
@@ -397,26 +364,17 @@ class TestClipboard:
         with pytest.raises(NotImplementedError, match=msg):
             read_clipboard(encoding="ascii")
 
-    @pytest.mark.parametrize("enc", ["UTF-8", "utf-8", "utf8"])
-    def test_round_trip_valid_encodings(self, enc, df):
-        self.check_round_trip_frame(df, encoding=enc)
-
-    @pytest.mark.single_cpu
     @pytest.mark.parametrize("data", ["\U0001f44d...", "Ωœ∑`...", "abcd..."])
-    @pytest.mark.xfail(
-        (os.environ.get("DISPLAY") is None and not is_platform_mac())
-        or is_ci_environment(),
-        reason="Cannot pass if a headless system is not put in place with Xvfb",
-        strict=not is_ci_environment(),  # Flaky failures in the CI
-    )
     def test_raw_roundtrip(self, data):
         # PR #25040 wide unicode wasn't copied correctly on PY3 on windows
-        clipboard_set(data)
-        assert data == clipboard_get()
+        df = DataFrame({"data": [data]})
+        df.to_clipboard()
+        result = read_clipboard()
+        tm.assert_frame_equal(df, result)
 
     @pytest.mark.parametrize("engine", ["c", "python"])
     def test_read_clipboard_dtype_backend(
-        self, request, mock_clipboard, string_storage, dtype_backend, engine
+        self, clipboard, string_storage, dtype_backend, engine
     ):
         # GH#50502
         if string_storage == "pyarrow" or dtype_backend == "pyarrow":
@@ -433,7 +391,7 @@ class TestClipboard:
         text = """a,b,c,d,e,f,g,h,i
 x,1,4.0,x,2,4.0,,True,False
 y,2,5.0,,,,,False,"""
-        mock_clipboard[request.node.name] = text
+        clipboard.setText(text)
 
         with pd.option_context("mode.string_storage", string_storage):
             result = read_clipboard(sep=",", dtype_backend=dtype_backend, engine=engine)