Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
119d3cd
fix: use `PyQt6` instead of `PyQt5`
Alvaro-Kothe Aug 22, 2025
d25b7bb
chore: use latest pyqt version available on conda repo
Alvaro-Kothe Aug 23, 2025
3f109b6
ci(deps): install PyQt6 with pip
Alvaro-Kothe Aug 24, 2025
ccc9b9a
fix: replace pyqt with pyqt6
Alvaro-Kothe Aug 24, 2025
58bd51a
chore: use correct capitalization for PyQt
Alvaro-Kothe Aug 24, 2025
13d75d3
docs(whatsnew): add PyQt6 support to whats new
Alvaro-Kothe Aug 24, 2025
1a7b249
docs(install): keep old pyqt5 minimum version
Alvaro-Kothe Aug 24, 2025
ec59ea1
fix: turn generator into tuple for correct error message
Alvaro-Kothe Aug 24, 2025
ce99f81
fix: let `getattr` raise `AttributeError` directly
Alvaro-Kothe Aug 25, 2025
e40d74c
Merge branch 'main' into fix/arm64-docker-build
Alvaro-Kothe Aug 25, 2025
7b32838
Merge remote-tracking branch 'main' into fix/arm64-docker-build
Alvaro-Kothe Aug 26, 2025
96aae6e
doc: remove extra space between bar and PyQt6
Alvaro-Kothe Aug 26, 2025
ada5ef6
Merge remote-tracking branch 'upstream/main' into fix/arm64-docker-build
Alvaro-Kothe Sep 7, 2025
2a7ebf7
docs(install): remove `PyQt4` dependency
Alvaro-Kothe Sep 7, 2025
31b17eb
test(clipboard): parametrize qt tests to use multiple versions
Alvaro-Kothe Sep 7, 2025
13adf24
test: add xfail mark when a qt version isn't available
Alvaro-Kothe Sep 7, 2025
6ab6534
ci: re-add PyQt5 as a CI dependency
Alvaro-Kothe Sep 7, 2025
7f4f993
ci(deps): rename PyQt5 to pyqt
Alvaro-Kothe Sep 7, 2025
1568f0c
test: be more specific about QtWidgets import
Alvaro-Kothe Sep 7, 2025
1fca531
test: fix `ModuleNotFoundError`
Alvaro-Kothe Sep 7, 2025
49734bf
test: match clipboard with PyQt
Alvaro-Kothe Sep 7, 2025
d0012e8
fix: fix PyQt5 dependency generator for conda
Alvaro-Kothe Sep 7, 2025
dafc4d0
ci: move dependencies and re-add pyqt5 to min_versions
Alvaro-Kothe Sep 8, 2025
01fd3f4
docs(install): make clipboard compatibility more explicit
Alvaro-Kothe Sep 8, 2025
4f5c05c
Merge remote-tracking branch 'upstream/main' into fix/arm64-docker-build
Alvaro-Kothe Sep 8, 2025
d009b80
Merge branch 'main' into fix/arm64-docker-build
Alvaro-Kothe Sep 8, 2025
a1c98df
test: undo changes in clipboard tests
Alvaro-Kothe Sep 11, 2025
d7028dc
chore: use `PyQt5` for minimum instead of `PyQt6`
Alvaro-Kothe Sep 11, 2025
2fb3e83
ci(clipboard): use `PyQt6` instead of `PyQt5` for non minimum tests
Alvaro-Kothe Sep 11, 2025
25db090
fix: remove pyqt5 from dev dependencies in favour of pyqt6
Alvaro-Kothe Sep 11, 2025
2b90647
ci(docker-build): build on arm64
Alvaro-Kothe Sep 13, 2025
72ca4e9
fix: add pyside6 support
Alvaro-Kothe Sep 15, 2025
d5df873
docs(clipboard): update note on clipboard dependencies
Alvaro-Kothe Sep 15, 2025
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
17 changes: 12 additions & 5 deletions .github/workflows/code-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,22 @@ jobs:
asv run --quick --dry-run --durations=30 --python=same --show-stderr

build_docker_dev_environment:
name: Build Docker Dev Environment
runs-on: ubuntu-24.04
name: Build Docker Dev Environment (${{ matrix.arch }})
runs-on: ${{ matrix.platform }}
strategy:
matrix:
include:
- arch: amd64
platform: ubuntu-24.04
- arch: arm64
platform: ubuntu-24.04-arm
defaults:
run:
shell: bash -el {0}

concurrency:
# https://github.community/t/concurrecy-not-work-for-push/183068/7
group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-build_docker_dev_environment
group: ${{ github.event_name == 'push' && github.run_number || github.ref }}-build_docker_dev_environment-${{ matrix.arch }}
cancel-in-progress: true

steps:
Expand All @@ -153,10 +160,10 @@ jobs:
fetch-depth: 0

- name: Build image
run: docker build --pull --no-cache --tag pandas-dev-env .
run: docker build --pull --no-cache --tag pandas-dev-env-${{ matrix.arch }} .

- name: Show environment
run: docker run --rm pandas-dev-env python -c "import pandas as pd; print(pd.show_versions())"
run: docker run --rm pandas-dev-env-${{ matrix.arch }} python -c "import pandas as pd; print(pd.show_versions())"

requirements-dev-text-installable:
name: Test install requirements-dev.txt
Expand Down
2 changes: 1 addition & 1 deletion ci/deps/actions-311.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ dependencies:
- numexpr>=2.10.2
- odfpy>=1.4.1
- qtpy>=2.4.2
- pyqt>=5.15.9
- openpyxl>=3.1.5
- psycopg2>=2.9.10
- pyarrow>=13.0.0
Expand All @@ -60,4 +59,5 @@ dependencies:
- zstandard>=0.23.0

- pip:
- PyQt6>=6.7.1
- tzdata>=2023.3
2 changes: 1 addition & 1 deletion ci/deps/actions-312.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ dependencies:
- numexpr>=2.10.2
- odfpy>=1.4.1
- qtpy>=2.4.2
- pyqt>=5.15.9
- openpyxl>=3.1.5
- psycopg2>=2.9.10
- pyarrow>=13.0.0
Expand All @@ -60,4 +59,5 @@ dependencies:
- zstandard>=0.23.0

- pip:
- PyQt6>=6.7.1
- tzdata>=2023.3
2 changes: 1 addition & 1 deletion ci/deps/actions-313-downstream_compat.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ dependencies:
- pyarrow>=13.0.0
- pyiceberg>=0.8.1
- pymysql>=1.1.1
- pyqt>=5.15.9
- pyreadstat>=1.2.8
- pytables>=3.10.1
- python-calamine>=0.3.0
Expand All @@ -71,4 +70,5 @@ dependencies:
- pandas-datareader
- pyyaml
- pip:
- PyQt6>=6.7.1
- tzdata>=2023.3
2 changes: 1 addition & 1 deletion ci/deps/actions-313.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ dependencies:
- numexpr>=2.10.2
- odfpy>=1.4.1
- qtpy>=2.4.2
- pyqt>=5.15.9
- openpyxl>=3.1.5
- psycopg2>=2.9.10
- pyarrow>=13.0.0
Expand All @@ -60,4 +59,5 @@ dependencies:
- zstandard>=0.23.0

- pip:
- PyQt6>=6.7.1
- tzdata>=2023.3
2 changes: 1 addition & 1 deletion doc/source/getting_started/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ Installable with ``pip install "pandas[clipboard]"``.
======================================================================================== ================== =============== ==============
Dependency Minimum Version pip extra Notes
======================================================================================== ================== =============== ==============
`PyQt4 <https://pypi.org/project/PyQt4/>`__/`PyQt5 <https://pypi.org/project/PyQt5/>`__ 5.15.9 clipboard Clipboard I/O
`PyQt5 <https://pypi.org/project/PyQt5/>`__/`PyQt6 <https://pypi.org/project/PyQt6/>`__ 5.15.9/6.7.1 clipboard Clipboard I/O
`qtpy <https://github.com/spyder-ide/qtpy>`__ 2.4.2 clipboard Clipboard I/O
======================================================================================== ================== =============== ==============

Expand Down
23 changes: 22 additions & 1 deletion doc/source/user_guide/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3928,7 +3928,28 @@ We can see that we got the same content back, which we had earlier written to th

.. note::

You may need to install xclip or xsel (with PyQt5, PyQt4 or qtpy) on Linux to use these methods.
On Linux, you may need to install one of these clipboard utilities:

- wl-clipboard (for Wayland sessions)
- xclip (for X11 sessions)
- xsel (for X11 sessions)

For example, on Debian-based systems:

.. code-block:: bash

sudo apt-get install wl-clipboard
sudo apt-get install xclip
sudo apt-get install xsel

Alternatively, you can install one of these Python packages,
but their support is limited on Linux and work more reliably
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The Qt clipboard documentation states that:

the X11 clipboard is event driven, i.e. the clipboard will not function properly if the event loop is not running. Similarly, it is recommended that the contents of the clipboard are stored or retrieved in direct response to user-input events, e.g. mouse button or key presses and releases. You should not store or retrieve the clipboard contents in response to timer or non-user-input events.

The way that pandas currently uses the clipboard on Linux doesn't trigger any event, which doesn't trigger any clipboard modification.

Since I am on Linux (Wayland), here is an example of how it is not reliable:

import sys
from PyQt6.QtWidgets import QApplication
from PyQt6.QtCore import QTimer, QEventLoop

app = QApplication(sys.argv)

app.clipboard().setText("Hello World!")
# CI tests this
print("clipboard before:", app.clipboard().text())
# clipboard before: Hello World!

# creates event loop and process events
loop = QEventLoop()
QTimer.singleShot(100, loop.quit)
loop.exec()

print("clipboard after :", app.clipboard().text())
# clipboard after :

Although the Qt bindings aren't reliable, the subprocess calling wl-copy works fine.

on OSX and Windows:

- PyQt6
- PySide6
- PyQt5


.. _io.pickle:

Expand Down
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ Other enhancements
- :meth:`pandas.concat` will raise a ``ValueError`` when ``ignore_index=True`` and ``keys`` is not ``None`` (:issue:`59274`)
- :py:class:`frozenset` elements in pandas objects are now natively printed (:issue:`60690`)
- Add ``"delete_rows"`` option to ``if_exists`` argument in :meth:`DataFrame.to_sql` deleting all records of the table before inserting data (:issue:`37210`).
- Added PyQt6 support to resolve ARM64 container build issues (:issue:`61037`)
- Added half-year offset classes :class:`HalfYearBegin`, :class:`HalfYearEnd`, :class:`BHalfYearBegin` and :class:`BHalfYearEnd` (:issue:`60928`)
- Added support to read and write from and to Apache Iceberg tables with the new :func:`read_iceberg` and :meth:`DataFrame.to_iceberg` functions (:issue:`61383`)
- Errors occurring during SQL I/O will now throw a generic :class:`.DatabaseError` instead of the raw Exception type from the underlying driver manager library (:issue:`60748`)
Expand Down
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ dependencies:
- pytest-xdist>=3.4.0
- pytest-qt>=4.4.0
- pytest-localserver
- pyqt>=5.15.9
- coverage

# required dependencies
Expand Down Expand Up @@ -122,4 +121,5 @@ dependencies:
- jupyterlite-pyodide-kernel

- pip:
- PyQt6>=6.7.1 # test dependency
- tzdata>=2023.3
81 changes: 51 additions & 30 deletions pandas/io/clipboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
sudo apt-get install xsel
sudo apt-get install wl-clipboard

Otherwise on Linux, you will need the PyQt5 modules installed.
Otherwise on Linux, you will need the PyQt6 modules installed.

This module does not work with PyGObject yet.

Expand Down Expand Up @@ -55,6 +55,7 @@
get_errno,
sizeof,
)
import importlib
import os
import platform
from shutil import which as _executable_exists
Expand Down Expand Up @@ -133,18 +134,52 @@ def paste_osx_pyobjc():
return copy_osx_pyobjc, paste_osx_pyobjc


def init_qt_clipboard():
global QApplication
# $DISPLAY should exist
def _import_module(modules: list[tuple[str, str | None]]):
"""
Attempt to import from a module from a list inorder.

Args:
modules: A list of tuples of two elements. The first element
is the module to import from and the second element is
the object to import. If the second element is not provided,
just import the module.

Returns:
The first successful import.

# Try to import from qtpy, but if that fails try PyQt5 then PyQt4
try:
from qtpy.QtWidgets import QApplication
except ImportError:
Raises:
ImportError: If couldn't import any module.
AttributeError: If a module doesn't have the expected attribute.
"""

for module_name, attribute_name in modules:
try:
from PyQt5.QtWidgets import QApplication
module = importlib.import_module(module_name)

if attribute_name is None:
return module
return getattr(module, attribute_name)

except ImportError:
from PyQt4.QtGui import QApplication
continue

raise ImportError(
f"No module from {tuple(module_name for module_name, _ in modules)} could be imported."
)


def init_qt_clipboard():
# $DISPLAY should exist
global QApplication

qt_qapplication_bindings = [
("qtpy.QtWidgets", "QApplication"),
("PyQt6.QtWidgets", "QApplication"),
("PySide6.QtWidgets", "QApplication"),
("PyQt5.QtWidgets", "QApplication"),
("PyQt4.QtGui", "QApplication"),
]
QApplication = _import_module(qt_qapplication_bindings)

app = QApplication.instance()
if app is None:
Expand Down Expand Up @@ -529,7 +564,7 @@ def determine_clipboard():
Determine the OS/platform and set the copy() and paste() functions
accordingly.
"""
global Foundation, AppKit, qtpy, PyQt4, PyQt5
global Foundation, AppKit

# Setup for the CYGWIN platform:
if (
Expand Down Expand Up @@ -576,25 +611,11 @@ def determine_clipboard():
return init_klipper_clipboard()

try:
# qtpy is a small abstraction layer that lets you write applications
# using a single api call to either PyQt or PySide.
# https://pypi.python.org/project/QtPy
import qtpy # check if qtpy is installed
except ImportError:
# If qtpy isn't installed, fall back on importing PyQt4.
try:
import PyQt5 # check if PyQt5 is installed
except ImportError:
try:
import PyQt4 # check if PyQt4 is installed
except ImportError:
pass # We want to fail fast for all non-ImportError exceptions.
else:
return init_qt_clipboard()
else:
return init_qt_clipboard()
else:
# Verify installation of pyqt, PyQt{6,5,4} and initialize its clipboard.
return init_qt_clipboard()
except ImportError:
# Ignore if Qt isn't available
pass

return init_no_clipboard()

Expand All @@ -618,7 +639,7 @@ def set_clipboard(clipboard):
clipboard_types = {
"pbcopy": init_osx_pbcopy_clipboard,
"pyobjc": init_osx_pyobjc_clipboard,
"qt": init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', and 'pyqt5'
"qt": init_qt_clipboard, # TODO - split this into 'qtpy', 'pyqt4', 'pyqt5' and 'pyqt6'
"xclip": init_xclip_clipboard,
"xsel": init_xsel_clipboard,
"wl-clipboard": init_wl_clipboard,
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ pytest-cov
pytest-xdist>=3.4.0
pytest-qt>=4.4.0
pytest-localserver
PyQt5>=5.15.9
coverage
python-dateutil
numpy<3
Expand Down Expand Up @@ -85,4 +84,5 @@ requests
pygments
jupyterlite-core
jupyterlite-pyodide-kernel
PyQt6>=6.7.1
tzdata>=2023.3
Loading