From 3761722922332d17cad2134ca5460a1ea367ad69 Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Thu, 23 Jun 2022 15:22:49 -0400 Subject: [PATCH 1/3] ENH: Move PyperclipException and PyperclipWindowsException to error/__init__.py per GH27656 --- doc/source/reference/testing.rst | 2 + pandas/conftest.py | 15 +++++++ pandas/errors/__init__.py | 21 ++++++++++ pandas/io/clipboard/__init__.py | 17 +++----- pandas/tests/io/test_clipboard.py | 67 +++++++++++++++++++++++++++++++ pandas/tests/test_errors.py | 9 +++++ 6 files changed, 119 insertions(+), 12 deletions(-) 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/conftest.py b/pandas/conftest.py index dfe8c5f1778d3..57914d35901dc 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -15,6 +15,7 @@ - Data sets/files - Time zones - Dtypes +- Ctypes - Misc """ @@ -1692,6 +1693,20 @@ def any_skipna_inferred_dtype(request): return inferred_dtype, values +# ---------------------------------------------------------------- +# Ctypes +# ---------------------------------------------------------------- +@pytest.fixture +def mock_ctypes(monkeypatch): + """ """ + + def _mock_win_error(): + return "Window Error" + + # Set raising to False because WinError won't exist on non-windows platforms + monkeypatch.setattr("ctypes.WinError", _mock_win_error, raising=False) + + # ---------------------------------------------------------------- # Misc # ---------------------------------------------------------------- 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..2938a8d920661 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,66 @@ def df(request): raise ValueError +@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 type(text) in 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..9a369b91935eb 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -2,6 +2,7 @@ from pandas.errors import ( AbstractMethodError, + PyperclipWindowsException, UndefinedVariableError, ) @@ -28,6 +29,7 @@ "SettingWithCopyWarning", "NumExprClobberingError", "IndexingError", + "PyperclipException", ], ) def test_exception_importable(exc): @@ -70,6 +72,13 @@ def test_catch_undefined_variable_error(is_local): raise UndefinedVariableError(variable_name, is_local) +@pytest.mark.usefixtures("mock_ctypes") +def test_PyperclipWindowsException(): + msg = "test \\(Window Error\\)" + with pytest.raises(PyperclipWindowsException, match=msg): + raise PyperclipWindowsException("test") + + class Foo: @classmethod def classmethod(cls): From f7b4a753fe3f64d17456aa355f73a845b9bfd2fd Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Thu, 23 Jun 2022 15:30:32 -0400 Subject: [PATCH 2/3] ENH: add docstring to fixture --- pandas/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 57914d35901dc..34f1e05bdd789 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -1698,7 +1698,9 @@ def any_skipna_inferred_dtype(request): # ---------------------------------------------------------------- @pytest.fixture def mock_ctypes(monkeypatch): - """ """ + """ + Mocks WinError to help with testing the clipboard. + """ def _mock_win_error(): return "Window Error" From 1a54111ceaaceae09f48c2c2fa4d421e619b1793 Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Thu, 23 Jun 2022 23:14:34 -0400 Subject: [PATCH 3/3] ENH: apply feedback --- pandas/conftest.py | 17 ----------------- pandas/tests/io/test_clipboard.py | 17 ++++++++++++++++- pandas/tests/test_errors.py | 8 -------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/pandas/conftest.py b/pandas/conftest.py index 34f1e05bdd789..dfe8c5f1778d3 100644 --- a/pandas/conftest.py +++ b/pandas/conftest.py @@ -15,7 +15,6 @@ - Data sets/files - Time zones - Dtypes -- Ctypes - Misc """ @@ -1693,22 +1692,6 @@ def any_skipna_inferred_dtype(request): return inferred_dtype, values -# ---------------------------------------------------------------- -# Ctypes -# ---------------------------------------------------------------- -@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 - monkeypatch.setattr("ctypes.WinError", _mock_win_error, raising=False) - - # ---------------------------------------------------------------- # Misc # ---------------------------------------------------------------- diff --git a/pandas/tests/io/test_clipboard.py b/pandas/tests/io/test_clipboard.py index 2938a8d920661..cb34cb6678a67 100644 --- a/pandas/tests/io/test_clipboard.py +++ b/pandas/tests/io/test_clipboard.py @@ -117,6 +117,21 @@ 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): """ @@ -165,7 +180,7 @@ def _return_true(): def test_stringify_text(text): valid_types = (str, int, float, bool) - if type(text) in valid_types: + if isinstance(text, valid_types): result = _stringifyText(text) assert result == str(text) else: diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py index 9a369b91935eb..e0ce798fec021 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -2,7 +2,6 @@ from pandas.errors import ( AbstractMethodError, - PyperclipWindowsException, UndefinedVariableError, ) @@ -72,13 +71,6 @@ def test_catch_undefined_variable_error(is_local): raise UndefinedVariableError(variable_name, is_local) -@pytest.mark.usefixtures("mock_ctypes") -def test_PyperclipWindowsException(): - msg = "test \\(Window Error\\)" - with pytest.raises(PyperclipWindowsException, match=msg): - raise PyperclipWindowsException("test") - - class Foo: @classmethod def classmethod(cls):