diff --git a/doc/source/reference/testing.rst b/doc/source/reference/testing.rst index 2c419e8df9517..c3ce267ff9dc7 100644 --- a/doc/source/reference/testing.rst +++ b/doc/source/reference/testing.rst @@ -43,6 +43,8 @@ Exceptions and warnings errors.ParserError errors.ParserWarning errors.PerformanceWarning + errors.PyperclipException + errors.PyperclipWindowsException errors.SettingWithCopyError errors.SettingWithCopyWarning errors.SpecificationError diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index b8d9df16311d7..04d52e2c5853c 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -3,6 +3,8 @@ """ from __future__ import annotations +import ctypes + from pandas._config.config import OptionError # noqa:F401 from pandas._libs.tslibs import ( # noqa:F401 @@ -374,3 +376,22 @@ class IndexingError(Exception): >>> s.loc["a", "c", "d"] # doctest: +SKIP ... # IndexingError: Too many indexers """ + + +class PyperclipException(RuntimeError): + """ + Exception is raised when trying to use methods like to_clipboard() and + read_clipboard() on an unsupported OS/platform. + """ + + +class PyperclipWindowsException(PyperclipException): + """ + Exception is raised when pandas is unable to get access to the clipboard handle + due to some other window process is accessing it. + """ + + def __init__(self, message: str) -> None: + # attr only exists on Windows, so typing fails on other platforms + message += f" ({ctypes.WinError()})" # type: ignore[attr-defined] + super().__init__(message) diff --git a/pandas/io/clipboard/__init__.py b/pandas/io/clipboard/__init__.py index 6a39b20869497..27fb06dfb6023 100644 --- a/pandas/io/clipboard/__init__.py +++ b/pandas/io/clipboard/__init__.py @@ -58,6 +58,11 @@ import time import warnings +from pandas.errors import ( + PyperclipException, + PyperclipWindowsException, +) + # `import PyQt4` sys.exit()s if DISPLAY is not in the environment. # Thus, we need to detect the presence of $DISPLAY manually # and not load PyQt4 if it is absent. @@ -87,18 +92,6 @@ def _executable_exists(name): ) -# Exceptions -class PyperclipException(RuntimeError): - pass - - -class PyperclipWindowsException(PyperclipException): - def __init__(self, message) -> None: - # attr only exists on Windows, so typing fails on other platforms - message += f" ({ctypes.WinError()})" # type: ignore[attr-defined] - super().__init__(message) - - def _stringifyText(text) -> str: acceptedTypes = (str, int, float, bool) if not isinstance(text, acceptedTypes): diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py index 73e563fd2b743..cb34cb6678a67 100644 --- a/pandas/tests/io/test_clipboard.py +++ b/pandas/tests/io/test_clipboard.py @@ -3,6 +3,11 @@ import numpy as np import pytest +from pandas.errors import ( + PyperclipException, + PyperclipWindowsException, +) + from pandas import ( DataFrame, get_option, @@ -11,6 +16,8 @@ import pandas._testing as tm from pandas.io.clipboard import ( + CheckedCall, + _stringifyText, clipboard_get, clipboard_set, ) @@ -110,6 +117,81 @@ def df(request): raise ValueError +@pytest.fixture +def mock_ctypes(monkeypatch): + """ + Mocks WinError to help with testing the clipboard. + """ + + def _mock_win_error(): + return "Window Error" + + # Set raising to False because WinError won't exist on non-windows platforms + with monkeypatch.context() as m: + m.setattr("ctypes.WinError", _mock_win_error, raising=False) + yield + + +@pytest.mark.usefixtures("mock_ctypes") +def test_checked_call_with_bad_call(monkeypatch): + """ + Give CheckCall a function that returns a falsey value and + mock get_errno so it returns false so an exception is raised. + """ + + def _return_false(): + return False + + monkeypatch.setattr("pandas.io.clipboard.get_errno", lambda: True) + msg = f"Error calling {_return_false.__name__} \\(Window Error\\)" + + with pytest.raises(PyperclipWindowsException, match=msg): + CheckedCall(_return_false)() + + +@pytest.mark.usefixtures("mock_ctypes") +def test_checked_call_with_valid_call(monkeypatch): + """ + Give CheckCall a function that returns a truthy value and + mock get_errno so it returns true so an exception is not raised. + The function should return the results from _return_true. + """ + + def _return_true(): + return True + + monkeypatch.setattr("pandas.io.clipboard.get_errno", lambda: False) + + # Give CheckedCall a callable that returns a truthy value s + checked_call = CheckedCall(_return_true) + assert checked_call() is True + + +@pytest.mark.parametrize( + "text", + [ + "String_test", + True, + 1, + 1.0, + 1j, + ], +) +def test_stringify_text(text): + valid_types = (str, int, float, bool) + + if isinstance(text, valid_types): + result = _stringifyText(text) + assert result == str(text) + else: + msg = ( + "only str, int, float, and bool values " + f"can be copied to the clipboard, not {type(text).__name__}" + ) + with pytest.raises(PyperclipException, match=msg): + _stringifyText(text) + + @pytest.fixture def mock_clipboard(monkeypatch, request): """Fixture mocking clipboard IO. diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py index 7e3d5b43f3014..e0ce798fec021 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -28,6 +28,7 @@ "SettingWithCopyWarning", "NumExprClobberingError", "IndexingError", + "PyperclipException", ], ) def test_exception_importable(exc):