Skip to content

Commit 2744967

Browse files
dataxerikyehoshuadimarsky
authored andcommitted
ENH: Move PyperclipException and PyperclipWindowsException to error/_… (pandas-dev#47491)
* ENH: Move PyperclipException and PyperclipWindowsException to error/__init__.py per GH27656 * ENH: add docstring to fixture * ENH: apply feedback
1 parent c4add45 commit 2744967

File tree

5 files changed

+111
-12
lines changed

5 files changed

+111
-12
lines changed

doc/source/reference/testing.rst

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ Exceptions and warnings
4343
errors.ParserError
4444
errors.ParserWarning
4545
errors.PerformanceWarning
46+
errors.PyperclipException
47+
errors.PyperclipWindowsException
4648
errors.SettingWithCopyError
4749
errors.SettingWithCopyWarning
4850
errors.SpecificationError

pandas/errors/__init__.py

+21
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"""
44
from __future__ import annotations
55

6+
import ctypes
7+
68
from pandas._config.config import OptionError # noqa:F401
79

810
from pandas._libs.tslibs import ( # noqa:F401
@@ -374,3 +376,22 @@ class IndexingError(Exception):
374376
>>> s.loc["a", "c", "d"] # doctest: +SKIP
375377
... # IndexingError: Too many indexers
376378
"""
379+
380+
381+
class PyperclipException(RuntimeError):
382+
"""
383+
Exception is raised when trying to use methods like to_clipboard() and
384+
read_clipboard() on an unsupported OS/platform.
385+
"""
386+
387+
388+
class PyperclipWindowsException(PyperclipException):
389+
"""
390+
Exception is raised when pandas is unable to get access to the clipboard handle
391+
due to some other window process is accessing it.
392+
"""
393+
394+
def __init__(self, message: str) -> None:
395+
# attr only exists on Windows, so typing fails on other platforms
396+
message += f" ({ctypes.WinError()})" # type: ignore[attr-defined]
397+
super().__init__(message)

pandas/io/clipboard/__init__.py

+5-12
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@
5858
import time
5959
import warnings
6060

61+
from pandas.errors import (
62+
PyperclipException,
63+
PyperclipWindowsException,
64+
)
65+
6166
# `import PyQt4` sys.exit()s if DISPLAY is not in the environment.
6267
# Thus, we need to detect the presence of $DISPLAY manually
6368
# and not load PyQt4 if it is absent.
@@ -87,18 +92,6 @@ def _executable_exists(name):
8792
)
8893

8994

90-
# Exceptions
91-
class PyperclipException(RuntimeError):
92-
pass
93-
94-
95-
class PyperclipWindowsException(PyperclipException):
96-
def __init__(self, message) -> None:
97-
# attr only exists on Windows, so typing fails on other platforms
98-
message += f" ({ctypes.WinError()})" # type: ignore[attr-defined]
99-
super().__init__(message)
100-
101-
10295
def _stringifyText(text) -> str:
10396
acceptedTypes = (str, int, float, bool)
10497
if not isinstance(text, acceptedTypes):

pandas/tests/io/test_clipboard.py

+82
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
import numpy as np
44
import pytest
55

6+
from pandas.errors import (
7+
PyperclipException,
8+
PyperclipWindowsException,
9+
)
10+
611
from pandas import (
712
DataFrame,
813
get_option,
@@ -11,6 +16,8 @@
1116
import pandas._testing as tm
1217

1318
from pandas.io.clipboard import (
19+
CheckedCall,
20+
_stringifyText,
1421
clipboard_get,
1522
clipboard_set,
1623
)
@@ -110,6 +117,81 @@ def df(request):
110117
raise ValueError
111118

112119

120+
@pytest.fixture
121+
def mock_ctypes(monkeypatch):
122+
"""
123+
Mocks WinError to help with testing the clipboard.
124+
"""
125+
126+
def _mock_win_error():
127+
return "Window Error"
128+
129+
# Set raising to False because WinError won't exist on non-windows platforms
130+
with monkeypatch.context() as m:
131+
m.setattr("ctypes.WinError", _mock_win_error, raising=False)
132+
yield
133+
134+
135+
@pytest.mark.usefixtures("mock_ctypes")
136+
def test_checked_call_with_bad_call(monkeypatch):
137+
"""
138+
Give CheckCall a function that returns a falsey value and
139+
mock get_errno so it returns false so an exception is raised.
140+
"""
141+
142+
def _return_false():
143+
return False
144+
145+
monkeypatch.setattr("pandas.io.clipboard.get_errno", lambda: True)
146+
msg = f"Error calling {_return_false.__name__} \\(Window Error\\)"
147+
148+
with pytest.raises(PyperclipWindowsException, match=msg):
149+
CheckedCall(_return_false)()
150+
151+
152+
@pytest.mark.usefixtures("mock_ctypes")
153+
def test_checked_call_with_valid_call(monkeypatch):
154+
"""
155+
Give CheckCall a function that returns a truthy value and
156+
mock get_errno so it returns true so an exception is not raised.
157+
The function should return the results from _return_true.
158+
"""
159+
160+
def _return_true():
161+
return True
162+
163+
monkeypatch.setattr("pandas.io.clipboard.get_errno", lambda: False)
164+
165+
# Give CheckedCall a callable that returns a truthy value s
166+
checked_call = CheckedCall(_return_true)
167+
assert checked_call() is True
168+
169+
170+
@pytest.mark.parametrize(
171+
"text",
172+
[
173+
"String_test",
174+
True,
175+
1,
176+
1.0,
177+
1j,
178+
],
179+
)
180+
def test_stringify_text(text):
181+
valid_types = (str, int, float, bool)
182+
183+
if isinstance(text, valid_types):
184+
result = _stringifyText(text)
185+
assert result == str(text)
186+
else:
187+
msg = (
188+
"only str, int, float, and bool values "
189+
f"can be copied to the clipboard, not {type(text).__name__}"
190+
)
191+
with pytest.raises(PyperclipException, match=msg):
192+
_stringifyText(text)
193+
194+
113195
@pytest.fixture
114196
def mock_clipboard(monkeypatch, request):
115197
"""Fixture mocking clipboard IO.

pandas/tests/test_errors.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"SettingWithCopyWarning",
2929
"NumExprClobberingError",
3030
"IndexingError",
31+
"PyperclipException",
3132
],
3233
)
3334
def test_exception_importable(exc):

0 commit comments

Comments
 (0)