Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 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
2 changes: 1 addition & 1 deletion ci/deps/actions-311-minimum_versions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ dependencies:
- pyarrow=12.0.1
- pyiceberg=0.7.1
- pymysql=1.1.0
- pyqt=5.15.9
- pyreadstat=1.2.6
- pytables=3.8.0
- python-calamine=0.1.7
Expand All @@ -62,4 +61,5 @@ dependencies:
- zstandard=0.22.0

- pip:
- PyQt6==6.7.1
- tzdata==2023.3
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.9.0
- odfpy>=1.4.1
- qtpy>=2.3.0
- pyqt>=5.15.9
- openpyxl>=3.1.2
- psycopg2>=2.9.9
- pyarrow>=12.0.1
Expand All @@ -60,4 +59,5 @@ dependencies:
- zstandard>=0.22.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.9.0
- odfpy>=1.4.1
- qtpy>=2.3.0
- pyqt>=5.15.9
- openpyxl>=3.1.2
- psycopg2>=2.9.9
- pyarrow>=12.0.1
Expand All @@ -60,4 +59,5 @@ dependencies:
- zstandard>=0.22.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>=12.0.1
- pyiceberg>=0.7.1
- pymysql>=1.1.0
- pyqt>=5.15.9
- pyreadstat>=1.2.6
- pytables>=3.8.0
- python-calamine>=0.1.7
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.9.0
- odfpy>=1.4.1
- qtpy>=2.3.0
- pyqt>=5.15.9
- openpyxl>=3.1.2
- psycopg2>=2.9.9
- pyarrow>=12.0.1
Expand All @@ -60,4 +59,5 @@ dependencies:
- zstandard>=0.22.0

- pip:
- PyQt6>=6.7.1
- tzdata>=2023.3
12 changes: 6 additions & 6 deletions doc/source/getting_started/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -340,12 +340,12 @@ Clipboard

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
`qtpy <https://github.com/spyder-ide/qtpy>`__ 2.3.0 clipboard Clipboard I/O
======================================================================================== ================== =============== ==============
======================================================================================================================================== ================== =============== ==============
Dependency Minimum Version pip extra Notes
======================================================================================================================================== ================== =============== ==============
`PyQt4 <https://pypi.org/project/PyQt4/>`__/`PyQt5 <https://pypi.org/project/PyQt5/>`__/`PyQt6 <https://pypi.org/project/PyQt6/>`__ 5.15.9 clipboard Clipboard I/O
Copy link
Member

Choose a reason for hiding this comment

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

You're listing the minimum version as 5.15.9 while changing the minimum-version action to PyQt6. In addition, it seems like PyQt4 should no longer be listed?

Copy link
Member Author

Choose a reason for hiding this comment

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

This change just extends the support to PyQt6, while keeping PyQt4 and PyQt5 compatibility intact.

it seems like PyQt4 should no longer be listed?

It still supports PyQt4, so I think it should remain, but if you want me to de-list it I will do it.

You're listing the minimum version as 5.15.9 while changing the minimum-version action to PyQt6

The goal of this change was just to replace the PyQt5 entry in requirements-dev.txt, the changes in the CI dependencies were unintentional, but required to test the PyQt6 changes in CI.

Pandas remains compatible with PyQt4 and PyQt5. The problem is that it is not being tested in CI anymore. Maybe we can list PyQt4, PyQt5 and PyQt6 in the CI dependencies and parametrize the qt clipboard tests to test the different PyQt versions?

Copy link
Member Author

Choose a reason for hiding this comment

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

It seems that PyQt4 is not available on conda-forge and I also can't download it from pypi. I will remove it from install.rst

Copy link
Member

Choose a reason for hiding this comment

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

The goal of this change was just to replace the PyQt5 entry in requirements-dev.txt, the changes in the CI dependencies were unintentional, but required to test the PyQt6 changes in CI.

We have multiple setup actions to support this - actions-311-minimum_versions.yaml could test with PyQt5 whereas actions-311.yaml could test with PyQt6.

Copy link
Member Author

@Alvaro-Kothe Alvaro-Kothe Sep 8, 2025

Choose a reason for hiding this comment

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

Thanks for the suggestion!

I was avoiding creating a big discrepancy between the CI jobs and ended up installing qtpy, PyQt5 and PyQt6 in all environments and parametrized the tests to use each of the Qt bindings. The problem with this approach is that the qapp fixture from pytest-qt doesn't handle the clipboard properly when the test is run with PyQt5, because qapp uses PyQt6 to create the clipboard, and, for some reason, it can't read what's written with PyQt5.

Anyway, the parametrization approach works but creates complexity. The advantage of this is that every CI job tests PyQt6 and PyQt5.

Splitting the CI is simpler and doesn't complicate the clipboard tests. I will leave it up to you the choice of:

  1. Test PyQt5 in actions-311-minimum_versions.yaml and test PyQt6 in the rest.
  2. Leave the clipboard tests parametrized.

Copy link
Member

Choose a reason for hiding this comment

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

I do not see the downside of having some CI jobs test PyQt5 and others PyQt6. cc @mroeschke for any thoughts here too.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've removed the test parametrization and restored the actions-311-minimum_versions.yaml file to its original state, where it tests PyQt5 while the other CI jobs test PyQt6.

`qtpy <https://github.com/spyder-ide/qtpy>`__ 2.3.0 clipboard Clipboard I/O
======================================================================================================================================== ================== =============== ==============

.. note::

Expand Down
2 changes: 1 addition & 1 deletion doc/source/user_guide/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3928,7 +3928,7 @@ 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.
You may need to install xclip or xsel (with PyQt6, PyQt5, PyQt4 or qtpy) on Linux to use these methods.

.. _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
- tzdata>=2023.3
2 changes: 1 addition & 1 deletion pandas/compat/_optional.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"xlsxwriter": "3.2.0",
"zstandard": "0.22.0",
"qtpy": "2.3.0",
"pyqt5": "5.15.9",
"PyQt6": "6.7.1",
}

# A mapping from import name to package name (on PyPI) for packages where
Expand Down
80 changes: 50 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,51 @@ 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"),
("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 +563,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 +610,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 +638,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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ html = ['beautifulsoup4>=4.12.3', 'html5lib>=1.1', 'lxml>=4.9.2']
xml = ['lxml>=4.9.2']
plot = ['matplotlib>=3.8.3']
output-formatting = ['jinja2>=3.1.3', 'tabulate>=0.9.0']
clipboard = ['PyQt5>=5.15.9', 'qtpy>=2.3.0']
clipboard = ['PyQt6>=6.7.1', 'qtpy>=2.3.0']
compression = ['zstandard>=0.22.0']
timezone = ['pytz>=2023.4']
all = ['adbc-driver-postgresql>=1.2.0',
Expand All @@ -99,7 +99,7 @@ all = ['adbc-driver-postgresql>=1.2.0',
'pyarrow>=12.0.1',
'pyiceberg>=0.7.1',
'pymysql>=1.1.0',
'PyQt5>=5.15.9',
'PyQt6>=6.7.1',
'pyreadstat>=1.2.6',
'pytest>=7.3.2',
'pytest-xdist>=3.4.0',
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
2 changes: 1 addition & 1 deletion scripts/generate_pip_deps_from_conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"dask-core": "dask",
"seaborn-base": "seaborn",
"sqlalchemy": "SQLAlchemy",
"pyqt": "PyQt5",
"pyqt": "PyQt6",
}


Expand Down
4 changes: 2 additions & 2 deletions scripts/tests/data/deps_minimum.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ html = ['beautifulsoup4>=4.9.3', 'html5lib>=1.1', 'lxml>=4.6.3']
xml = ['lxml>=4.6.3']
plot = ['matplotlib>=3.6.1']
output_formatting = ['jinja2>=3.0.0', 'tabulate>=0.8.9']
clipboard = ['PyQt5>=5.15.1', 'qtpy>=2.3.0']
clipboard = ['PyQt6>=6.7.1', 'qtpy>=2.3.0']
compression = ['zstandard>=0.15.2']
all = ['beautifulsoup4>=5.9.3',
'bottleneck>=1.3.2',
Expand All @@ -90,7 +90,7 @@ all = ['beautifulsoup4>=5.9.3',
'psycopg2>=2.9.9',
'pyarrow>=7.0.0',
'pymysql>=1.1.0',
'PyQt5>=5.15.1',
'PyQt6>=6.7.1',
'pyreadstat>=1.1.2',
'pytest>=7.3.2',
'pytest-xdist>=3.4.0',
Expand Down
Loading