From 4b1224a7c9db8cf9a73c690d15b308cc4b13eca4 Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Mon, 15 Aug 2022 12:09:32 -0400 Subject: [PATCH 1/9] ENH: move an exception and add a prehook to check for exception placement --- .pre-commit-config.yaml | 8 ++ doc/source/reference/testing.rst | 1 + doc/source/whatsnew/v1.5.0.rst | 2 +- pandas/errors/__init__.py | 7 ++ pandas/tests/test_errors.py | 1 + pandas/util/version/__init__.py | 8 +- .../tests/test_validate_exception_location.py | 46 ++++++++ scripts/validate_exception_location.py | 109 ++++++++++++++++++ 8 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 scripts/tests/test_validate_exception_location.py create mode 100644 scripts/validate_exception_location.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ca5b5c9b896b..52a702c33a054 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -232,6 +232,14 @@ repos: entry: python scripts/validate_min_versions_in_sync.py language: python files: ^(ci/deps/actions-.*-minimum_versions\.yaml|pandas/compat/_optional\.py)$ + - id: validate-errors-locations + name: Validate errors locations + description: Validate errors are in approriate locations. + entry: python scripts/validate_exception_location.py + language: python + files: ^pandas/ + exclude: ^(pandas/_libs/|pandas/tests/|pandas/errors/__init__.py$) + types: [python] - id: flake8-pyi name: flake8-pyi entry: flake8 --extend-ignore=E301,E302,E305,E701,E704 diff --git a/doc/source/reference/testing.rst b/doc/source/reference/testing.rst index 1144c767942d4..ddb1af1efa493 100644 --- a/doc/source/reference/testing.rst +++ b/doc/source/reference/testing.rst @@ -39,6 +39,7 @@ Exceptions and warnings errors.IndexingError errors.InvalidColumnName errors.InvalidIndexError + errors.InvalidVersion errors.IntCastingNaNError errors.MergeError errors.NullFrequencyError diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index b71d294b97f9a..f80b82b899e40 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -285,7 +285,7 @@ Other enhancements - A :class:`errors.PerformanceWarning` is now thrown when using ``string[pyarrow]`` dtype with methods that don't dispatch to ``pyarrow.compute`` methods (:issue:`42613`) - Added ``numeric_only`` argument to :meth:`Resampler.sum`, :meth:`Resampler.prod`, :meth:`Resampler.min`, :meth:`Resampler.max`, :meth:`Resampler.first`, and :meth:`Resampler.last` (:issue:`46442`) - ``times`` argument in :class:`.ExponentialMovingWindow` now accepts ``np.timedelta64`` (:issue:`47003`) -- :class:`.DataError`, :class:`.SpecificationError`, :class:`.SettingWithCopyError`, :class:`.SettingWithCopyWarning`, :class:`.NumExprClobberingError`, :class:`.UndefinedVariableError`, :class:`.IndexingError`, :class:`.PyperclipException`, :class:`.PyperclipWindowsException`, :class:`.CSSWarning`, :class:`.PossibleDataLossError`, :class:`.ClosedFileError`, :class:`.IncompatibilityWarning`, :class:`.AttributeConflictWarning`, :class:`.DatabaseError, :class:`.PossiblePrecisionLoss, :class:`.ValueLabelTypeMismatch, :class:`.InvalidColumnName, and :class:`.CategoricalConversionWarning` are now exposed in ``pandas.errors`` (:issue:`27656`) +- :class:`.DataError`, :class:`.SpecificationError`, :class:`.SettingWithCopyError`, :class:`.SettingWithCopyWarning`, :class:`.NumExprClobberingError`, :class:`.UndefinedVariableError`, :class:`.IndexingError`, :class:`.PyperclipException`, :class:`.PyperclipWindowsException`, :class:`.CSSWarning`, :class:`.PossibleDataLossError`, :class:`.ClosedFileError`, :class:`.IncompatibilityWarning`, :class:`.AttributeConflictWarning`, :class:`.DatabaseError, :class:`.PossiblePrecisionLoss, :class:`.ValueLabelTypeMismatch, :class:`.InvalidColumnName, :class:`.CategoricalConversionWarning`, and :class:`.InvalidVersion` are now exposed in ``pandas.errors`` (:issue:`27656`) - Added ``check_like`` argument to :func:`testing.assert_series_equal` (:issue:`47247`) - Allow reading compressed SAS files with :func:`read_sas` (e.g., ``.sas7bdat.gz`` files) - :meth:`DatetimeIndex.astype` now supports casting timezone-naive indexes to ``datetime64[s]``, ``datetime64[ms]``, and ``datetime64[us]``, and timezone-aware indexes to the corresponding ``datetime64[unit, tzname]`` dtypes (:issue:`47579`) diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index d0c9ef94f4453..978c7bd47fba7 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -535,6 +535,12 @@ class CategoricalConversionWarning(Warning): """ +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + __all__ = [ "AbstractMethodError", "AccessorRegistrationWarning", @@ -551,6 +557,7 @@ class CategoricalConversionWarning(Warning): "IntCastingNaNError", "InvalidColumnName", "InvalidIndexError", + "InvalidVersion", "IndexingError", "MergeError", "NullFrequencyError", diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py index c6ca51b7763d9..a5a08a37d5630 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -39,6 +39,7 @@ "CategoricalConversionWarning", "InvalidColumnName", "ValueLabelTypeMismatch", + "InvalidVersion", ], ) def test_exception_importable(exc): diff --git a/pandas/util/version/__init__.py b/pandas/util/version/__init__.py index a6eccf2941342..199e95617efdb 100644 --- a/pandas/util/version/__init__.py +++ b/pandas/util/version/__init__.py @@ -20,6 +20,8 @@ ) import warnings +from errors import InvalidVersion + __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] @@ -126,12 +128,6 @@ def parse(version: str) -> LegacyVersion | Version: return LegacyVersion(version) -class InvalidVersion(ValueError): - """ - An invalid version was found, users should refer to PEP 440. - """ - - class _BaseVersion: _key: CmpKey | LegacyCmpKey diff --git a/scripts/tests/test_validate_exception_location.py b/scripts/tests/test_validate_exception_location.py new file mode 100644 index 0000000000000..c988cc6be9dd1 --- /dev/null +++ b/scripts/tests/test_validate_exception_location.py @@ -0,0 +1,46 @@ +import pytest + +from scripts.test_validate_exception_location.py import ( + validate_exception_and_warning_placement, +) + +PATH = "t.py" +CUSTOM_EXCEPTION = "MyException" + +TEST_CODE = """ +import numpy as np +import sys + +def my_func(): + pass + +class {custom_name}({error_type}): + pass + +""" + +testdata = [ + "Exception", + "ValueError", + "Warning", + "UserWarning", +] + + +@pytest.mark.parametrize("error_type", testdata) +def test_class_that_inherits_an_exception_is_flagged(capsys, error_type): + content = TEST_CODE.format(custom_name=CUSTOM_EXCEPTION, error_type=error_type) + result_msg = ( + "t.py:8:0: {exception_name}: Please don't place exceptions or " + "warnings outside of pandas/errors/__init__.py or " + "pandas/_libs\n".format(exception_name=CUSTOM_EXCEPTION) + ) + with pytest.raises(SystemExit, match=None): + validate_exception_and_warning_placement(PATH, content) + expected_msg, _ = capsys.readouterr() + assert result_msg == expected_msg + + +def test_class_that_does_not_inherit_an_exception_is_flagged(capsys): + content = "class MyClass(NonExceptionClass): pass" + validate_exception_and_warning_placement(PATH, content) diff --git a/scripts/validate_exception_location.py b/scripts/validate_exception_location.py new file mode 100644 index 0000000000000..8eb59116aa807 --- /dev/null +++ b/scripts/validate_exception_location.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import argparse +import ast +import sys +from typing import Sequence + +ERROR_MESSAGE = ( + "{path}:{lineno}:{col_offset}: {exception_name}: " + "Please don't place exceptions or warnings outside of pandas/errors/__init__.py or " + "pandas/_libs\n" +) +exception_warning_list = { + "ArithmeticError", + "AssertionError", + "AttributeError", + "EOFError", + "Exception", + "FloatingPointError", + "GeneratorExit", + "ImportError", + "IndentationError", + "IndexError", + "KeyboardInterrupt", + "KeyError", + "LookupError", + "MemoryError", + "NameError", + "NotImplementedError", + "OSError", + "OverflowError", + "ReferenceError", + "RuntimeError", + "StopIteration", + "SyntaxError", + "SystemError", + "SystemExit", + "TabError", + "TypeError", + "UnboundLocalError", + "UnicodeDecodeError", + "UnicodeEncodeError", + "UnicodeError", + "UnicodeTranslateError", + "ValueError", + "ZeroDivisionError", + "BytesWarning", + "DeprecationWarning", + "FutureWarning", + "ImportWarning", + "PendingDeprecationWarning", + "ResourceWarning", + "RuntimeWarning", + "SyntaxWarning", + "UnicodeWarning", + "UserWarning", + "Warning", +} + +permisable_exception_warning_list = [ + "LossySetitemError", + "NoBufferPresent", + "InvalidComparison", + "NotThisMethod", + "OptionError", +] + + +class Visitor(ast.NodeVisitor): + def __init__(self, path: str) -> None: + self.path = path + + def visit_ClassDef(self, node): + classes = {getattr(n, "id", None) for n in node.bases} + + if ( + classes + and classes.issubset(exception_warning_list) + and node.name not in permisable_exception_warning_list + ): + msg = ERROR_MESSAGE.format( + path=self.path, + lineno=node.lineno, + col_offset=node.col_offset, + exception_name=node.name, + ) + sys.stdout.write(msg) + sys.exit(1) + + +def validate_exception_and_warning_placement(file_path: str, file_content: str): + tree = ast.parse(file_content) + visitor = Visitor(file_path) + visitor.visit(tree) + + +def main(argv: Sequence[str] | None = None) -> None: + parser = argparse.ArgumentParser() + parser.add_argument("paths", nargs="*") + args = parser.parse_args(argv) + + for path in args.paths: + with open(path, encoding="utf-8") as fd: + content = fd.read() + validate_exception_and_warning_placement(path, content) + + +if __name__ == "__main__": + main() From 4c67ef2ae6a2182f62d663917d042bafc195f36c Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Mon, 15 Aug 2022 13:13:16 -0400 Subject: [PATCH 2/9] ENH: fix import --- pandas/util/version/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/util/version/__init__.py b/pandas/util/version/__init__.py index 199e95617efdb..2f6dc2b425b71 100644 --- a/pandas/util/version/__init__.py +++ b/pandas/util/version/__init__.py @@ -20,7 +20,7 @@ ) import warnings -from errors import InvalidVersion +from pandas.errors import InvalidVersion __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] From 7f9ea074d32885e931fd66d74ac4681735c74154 Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Mon, 15 Aug 2022 14:21:21 -0400 Subject: [PATCH 3/9] ENH: revert moving error --- doc/source/reference/testing.rst | 1 - doc/source/whatsnew/v1.5.0.rst | 2 +- pandas/errors/__init__.py | 7 ------- pandas/tests/test_errors.py | 1 - pandas/util/version/__init__.py | 8 ++++++-- scripts/validate_exception_location.py | 1 + 6 files changed, 8 insertions(+), 12 deletions(-) diff --git a/doc/source/reference/testing.rst b/doc/source/reference/testing.rst index ddb1af1efa493..1144c767942d4 100644 --- a/doc/source/reference/testing.rst +++ b/doc/source/reference/testing.rst @@ -39,7 +39,6 @@ Exceptions and warnings errors.IndexingError errors.InvalidColumnName errors.InvalidIndexError - errors.InvalidVersion errors.IntCastingNaNError errors.MergeError errors.NullFrequencyError diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index f80b82b899e40..b71d294b97f9a 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -285,7 +285,7 @@ Other enhancements - A :class:`errors.PerformanceWarning` is now thrown when using ``string[pyarrow]`` dtype with methods that don't dispatch to ``pyarrow.compute`` methods (:issue:`42613`) - Added ``numeric_only`` argument to :meth:`Resampler.sum`, :meth:`Resampler.prod`, :meth:`Resampler.min`, :meth:`Resampler.max`, :meth:`Resampler.first`, and :meth:`Resampler.last` (:issue:`46442`) - ``times`` argument in :class:`.ExponentialMovingWindow` now accepts ``np.timedelta64`` (:issue:`47003`) -- :class:`.DataError`, :class:`.SpecificationError`, :class:`.SettingWithCopyError`, :class:`.SettingWithCopyWarning`, :class:`.NumExprClobberingError`, :class:`.UndefinedVariableError`, :class:`.IndexingError`, :class:`.PyperclipException`, :class:`.PyperclipWindowsException`, :class:`.CSSWarning`, :class:`.PossibleDataLossError`, :class:`.ClosedFileError`, :class:`.IncompatibilityWarning`, :class:`.AttributeConflictWarning`, :class:`.DatabaseError, :class:`.PossiblePrecisionLoss, :class:`.ValueLabelTypeMismatch, :class:`.InvalidColumnName, :class:`.CategoricalConversionWarning`, and :class:`.InvalidVersion` are now exposed in ``pandas.errors`` (:issue:`27656`) +- :class:`.DataError`, :class:`.SpecificationError`, :class:`.SettingWithCopyError`, :class:`.SettingWithCopyWarning`, :class:`.NumExprClobberingError`, :class:`.UndefinedVariableError`, :class:`.IndexingError`, :class:`.PyperclipException`, :class:`.PyperclipWindowsException`, :class:`.CSSWarning`, :class:`.PossibleDataLossError`, :class:`.ClosedFileError`, :class:`.IncompatibilityWarning`, :class:`.AttributeConflictWarning`, :class:`.DatabaseError, :class:`.PossiblePrecisionLoss, :class:`.ValueLabelTypeMismatch, :class:`.InvalidColumnName, and :class:`.CategoricalConversionWarning` are now exposed in ``pandas.errors`` (:issue:`27656`) - Added ``check_like`` argument to :func:`testing.assert_series_equal` (:issue:`47247`) - Allow reading compressed SAS files with :func:`read_sas` (e.g., ``.sas7bdat.gz`` files) - :meth:`DatetimeIndex.astype` now supports casting timezone-naive indexes to ``datetime64[s]``, ``datetime64[ms]``, and ``datetime64[us]``, and timezone-aware indexes to the corresponding ``datetime64[unit, tzname]`` dtypes (:issue:`47579`) diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index 978c7bd47fba7..d0c9ef94f4453 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -535,12 +535,6 @@ class CategoricalConversionWarning(Warning): """ -class InvalidVersion(ValueError): - """ - An invalid version was found, users should refer to PEP 440. - """ - - __all__ = [ "AbstractMethodError", "AccessorRegistrationWarning", @@ -557,7 +551,6 @@ class InvalidVersion(ValueError): "IntCastingNaNError", "InvalidColumnName", "InvalidIndexError", - "InvalidVersion", "IndexingError", "MergeError", "NullFrequencyError", diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py index a5a08a37d5630..c6ca51b7763d9 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -39,7 +39,6 @@ "CategoricalConversionWarning", "InvalidColumnName", "ValueLabelTypeMismatch", - "InvalidVersion", ], ) def test_exception_importable(exc): diff --git a/pandas/util/version/__init__.py b/pandas/util/version/__init__.py index 2f6dc2b425b71..a6eccf2941342 100644 --- a/pandas/util/version/__init__.py +++ b/pandas/util/version/__init__.py @@ -20,8 +20,6 @@ ) import warnings -from pandas.errors import InvalidVersion - __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] @@ -128,6 +126,12 @@ def parse(version: str) -> LegacyVersion | Version: return LegacyVersion(version) +class InvalidVersion(ValueError): + """ + An invalid version was found, users should refer to PEP 440. + """ + + class _BaseVersion: _key: CmpKey | LegacyCmpKey diff --git a/scripts/validate_exception_location.py b/scripts/validate_exception_location.py index 8eb59116aa807..b6f1bd2c74ae8 100644 --- a/scripts/validate_exception_location.py +++ b/scripts/validate_exception_location.py @@ -63,6 +63,7 @@ "InvalidComparison", "NotThisMethod", "OptionError", + "InvalidVersion", ] From b7793ceabf9182b224490d81ff88a138ef51a941 Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Mon, 15 Aug 2022 15:46:22 -0400 Subject: [PATCH 4/9] ENH: add docstring and fix import for test --- scripts/tests/test_validate_exception_location.py | 4 +--- scripts/validate_exception_location.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/scripts/tests/test_validate_exception_location.py b/scripts/tests/test_validate_exception_location.py index c988cc6be9dd1..25a8e3e9b7392 100644 --- a/scripts/tests/test_validate_exception_location.py +++ b/scripts/tests/test_validate_exception_location.py @@ -1,8 +1,6 @@ import pytest -from scripts.test_validate_exception_location.py import ( - validate_exception_and_warning_placement, -) +from scripts.validate_exception_location import validate_exception_and_warning_placement PATH = "t.py" CUSTOM_EXCEPTION = "MyException" diff --git a/scripts/validate_exception_location.py b/scripts/validate_exception_location.py index b6f1bd2c74ae8..c199a2f12edf7 100644 --- a/scripts/validate_exception_location.py +++ b/scripts/validate_exception_location.py @@ -1,3 +1,16 @@ +""" +Validate that the exceptions and warnings are in approrirate places. + +Checks for classes that inherit a python exception and warning and +flags them, unless they are exempted from checking. + +Print the exception/warning that do not follow convention. + +Usage:: + +As pre-commit hook (recommended): + pre-commit run validate-errors-locations --all-files +""" from __future__ import annotations import argparse From d40aee7682faccd9c5410f4e220404a2f0a03ce3 Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Tue, 6 Sep 2022 15:20:01 -0400 Subject: [PATCH 5/9] ENH: re-design approach based on feedback --- doc/source/reference/testing.rst | 5 ++ pandas/core/arrays/datetimelike.py | 10 +--- pandas/core/dtypes/cast.py | 13 ++--- pandas/core/interchange/column.py | 2 +- pandas/core/interchange/utils.py | 4 -- pandas/errors/__init__.py | 26 ++++++++++ pandas/tests/test_errors.py | 47 ++++++++++--------- scripts/pandas_errors_documented.py | 2 +- .../tests/test_validate_exception_location.py | 34 +++++++++++--- scripts/validate_exception_location.py | 45 +++++++++++------- 10 files changed, 120 insertions(+), 68 deletions(-) diff --git a/doc/source/reference/testing.rst b/doc/source/reference/testing.rst index 1144c767942d4..ca13c85eb7289 100644 --- a/doc/source/reference/testing.rst +++ b/doc/source/reference/testing.rst @@ -38,9 +38,14 @@ Exceptions and warnings errors.IncompatibilityWarning errors.IndexingError errors.InvalidColumnName + errors.InvalidComparison errors.InvalidIndexError + errors.InvalidVersion errors.IntCastingNaNError + errors.LossySetitemError errors.MergeError + errors.NoBufferPresent + errors.NotThisMethod errors.NullFrequencyError errors.NumbaUtilError errors.NumExprClobberingError diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 2c070499308a7..0b1039674247c 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -64,6 +64,7 @@ from pandas.compat.numpy import function as nv from pandas.errors import ( AbstractMethodError, + InvalidComparison, NullFrequencyError, PerformanceWarning, ) @@ -152,15 +153,6 @@ DatetimeLikeArrayT = TypeVar("DatetimeLikeArrayT", bound="DatetimeLikeArrayMixin") -class InvalidComparison(Exception): - """ - Raised by _validate_comparison_value to indicate to caller it should - return invalid_comparison. - """ - - pass - - class DatetimeLikeArrayMixin(OpsMixin, NDArrayBackedExtensionArray): """ Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray diff --git a/pandas/core/dtypes/cast.py b/pandas/core/dtypes/cast.py index 5340bc6b590c4..d9d95070add45 100644 --- a/pandas/core/dtypes/cast.py +++ b/pandas/core/dtypes/cast.py @@ -40,7 +40,10 @@ DtypeObj, Scalar, ) -from pandas.errors import IntCastingNaNError +from pandas.errors import ( + IntCastingNaNError, + LossySetitemError, +) from pandas.util._exceptions import find_stack_level from pandas.util._validators import validate_bool_kwarg @@ -2087,11 +2090,3 @@ def _dtype_can_hold_range(rng: range, dtype: np.dtype) -> bool: if not len(rng): return True return np.can_cast(rng[0], dtype) and np.can_cast(rng[-1], dtype) - - -class LossySetitemError(Exception): - """ - Raised when trying to do a __setitem__ on an np.ndarray that is not lossless. - """ - - pass diff --git a/pandas/core/interchange/column.py b/pandas/core/interchange/column.py index 83f57d5bb8d3e..644e4e07a8431 100644 --- a/pandas/core/interchange/column.py +++ b/pandas/core/interchange/column.py @@ -6,6 +6,7 @@ from pandas._libs.lib import infer_dtype from pandas._libs.tslibs import iNaT +from pandas.errors import NoBufferPresent from pandas.util._decorators import cache_readonly import pandas as pd @@ -23,7 +24,6 @@ from pandas.core.interchange.utils import ( ArrowCTypes, Endianness, - NoBufferPresent, dtype_to_arrow_c_fmt, ) diff --git a/pandas/core/interchange/utils.py b/pandas/core/interchange/utils.py index 1d56af94b2629..aa717d05aecb5 100644 --- a/pandas/core/interchange/utils.py +++ b/pandas/core/interchange/utils.py @@ -89,7 +89,3 @@ def dtype_to_arrow_c_fmt(dtype: DtypeObj) -> str: raise NotImplementedError( f"Conversion of {dtype} to Arrow C format string is not implemented." ) - - -class NoBufferPresent(Exception): - """Exception to signal that there is no requested buffer.""" diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index d0c9ef94f4453..f0bcf468b6f1a 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -12,6 +12,9 @@ OutOfBoundsTimedelta, ) +from pandas._version import NotThisMethod +from pandas.util.version import InvalidVersion + class IntCastingNaNError(ValueError): """ @@ -535,6 +538,24 @@ class CategoricalConversionWarning(Warning): """ +class LossySetitemError(Exception): + """ + Raised when trying to do a __setitem__ on an np.ndarray that is not lossless. + """ + + +class NoBufferPresent(Exception): + """ + Exception is raised in _get_data_buffer to signal that there is no requested buffer. + """ + + +class InvalidComparison(Exception): + """ + Exception is raised by _validate_comparison_value to indicate an invalid comparison. + """ + + __all__ = [ "AbstractMethodError", "AccessorRegistrationWarning", @@ -550,9 +571,14 @@ class CategoricalConversionWarning(Warning): "IncompatibilityWarning", "IntCastingNaNError", "InvalidColumnName", + "InvalidComparison", "InvalidIndexError", + "InvalidVersion", "IndexingError", + "LossySetitemError", "MergeError", + "NoBufferPresent", + "NotThisMethod", "NullFrequencyError", "NumbaUtilError", "NumExprClobberingError", diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py index c6ca51b7763d9..6e44d7c32b0cb 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -11,33 +11,38 @@ @pytest.mark.parametrize( "exc", [ - "UnsupportedFunctionCall", - "UnsortedIndexError", - "OutOfBoundsDatetime", - "ParserError", - "PerformanceWarning", + "AttributeConflictWarning", + "CSSWarning", + "CategoricalConversionWarning", + "ClosedFileError", + "DataError", + "DatabaseError", "DtypeWarning", "EmptyDataError", - "ParserWarning", + "IncompatibilityWarning", + "IndexingError", + "InvalidColumnName", + "InvalidComparison", + "InvalidVersion", + "LossySetitemError", "MergeError", - "OptionError", - "NumbaUtilError", - "DataError", - "SpecificationError", - "SettingWithCopyError", - "SettingWithCopyWarning", + "NoBufferPresent", + "NotThisMethod", "NumExprClobberingError", - "IndexingError", - "PyperclipException", - "CSSWarning", - "ClosedFileError", + "NumbaUtilError", + "OptionError", + "OutOfBoundsDatetime", + "ParserError", + "ParserWarning", + "PerformanceWarning", "PossibleDataLossError", - "IncompatibilityWarning", - "AttributeConflictWarning", - "DatabaseError", "PossiblePrecisionLoss", - "CategoricalConversionWarning", - "InvalidColumnName", + "PyperclipException", + "SettingWithCopyError", + "SettingWithCopyWarning", + "SpecificationError", + "UnsortedIndexError", + "UnsupportedFunctionCall", "ValueLabelTypeMismatch", ], ) diff --git a/scripts/pandas_errors_documented.py b/scripts/pandas_errors_documented.py index 18db5fa10a8f9..52c1e2008b8a0 100644 --- a/scripts/pandas_errors_documented.py +++ b/scripts/pandas_errors_documented.py @@ -1,5 +1,5 @@ """ -Check that doc/source/reference/general_utility_functions.rst documents +Check that doc/source/reference/testing.rst documents all exceptions and warnings in pandas/errors/__init__.py. This is meant to be run as a pre-commit hook - to run it manually, you can do: diff --git a/scripts/tests/test_validate_exception_location.py b/scripts/tests/test_validate_exception_location.py index 25a8e3e9b7392..1294d72743c61 100644 --- a/scripts/tests/test_validate_exception_location.py +++ b/scripts/tests/test_validate_exception_location.py @@ -3,7 +3,12 @@ from scripts.validate_exception_location import validate_exception_and_warning_placement PATH = "t.py" -CUSTOM_EXCEPTION = "MyException" + +# ERRORS_IN_TESTING_RST is the set returned when parsing testing.rst for all the +# exceptions and warnings. +CUSTOM_EXCEPTION_NOT_IN_TESTING_RST = "MyException" +CUSTOM_EXCEPTION__IN_TESTING_RST = "MyOldException" +ERRORS_IN_TESTING_RST = {CUSTOM_EXCEPTION__IN_TESTING_RST} TEST_CODE = """ import numpy as np @@ -17,6 +22,7 @@ class {custom_name}({error_type}): """ +# Test with various python-defined exceptions to ensure they are all flagged. testdata = [ "Exception", "ValueError", @@ -26,19 +32,33 @@ class {custom_name}({error_type}): @pytest.mark.parametrize("error_type", testdata) -def test_class_that_inherits_an_exception_is_flagged(capsys, error_type): - content = TEST_CODE.format(custom_name=CUSTOM_EXCEPTION, error_type=error_type) +def test_class_that_inherits_an_exception_and_is_not_in_the_testing_rst_is_flagged( + capsys, error_type +): + content = TEST_CODE.format( + custom_name=CUSTOM_EXCEPTION_NOT_IN_TESTING_RST, error_type=error_type + ) result_msg = ( "t.py:8:0: {exception_name}: Please don't place exceptions or " "warnings outside of pandas/errors/__init__.py or " - "pandas/_libs\n".format(exception_name=CUSTOM_EXCEPTION) + "pandas/_libs\n".format(exception_name=CUSTOM_EXCEPTION_NOT_IN_TESTING_RST) ) with pytest.raises(SystemExit, match=None): - validate_exception_and_warning_placement(PATH, content) + validate_exception_and_warning_placement(PATH, content, ERRORS_IN_TESTING_RST) expected_msg, _ = capsys.readouterr() assert result_msg == expected_msg -def test_class_that_does_not_inherit_an_exception_is_flagged(capsys): +@pytest.mark.parametrize("error_type", testdata) +def test_class_that_inherits_an_exception_but_is_in_the_testing_rst_is_not_flagged( + capsys, error_type +): + content = TEST_CODE.format( + custom_name=CUSTOM_EXCEPTION__IN_TESTING_RST, error_type=error_type + ) + validate_exception_and_warning_placement(PATH, content, ERRORS_IN_TESTING_RST) + + +def test_class_that_does_not_inherit_an_exception_is_not_flagged(capsys): content = "class MyClass(NonExceptionClass): pass" - validate_exception_and_warning_placement(PATH, content) + validate_exception_and_warning_placement(PATH, content, ERRORS_IN_TESTING_RST) diff --git a/scripts/validate_exception_location.py b/scripts/validate_exception_location.py index c199a2f12edf7..9da6056048444 100644 --- a/scripts/validate_exception_location.py +++ b/scripts/validate_exception_location.py @@ -2,22 +2,31 @@ Validate that the exceptions and warnings are in approrirate places. Checks for classes that inherit a python exception and warning and -flags them, unless they are exempted from checking. +flags them, unless they are exempted from checking. Exempt meaning +the exception/warning is defined in testing.rst. Testing.rst contains +a list of pandas defined exceptions and warnings. This list is kept +current by other pre-commit hook, pandas_errors_documented.py. +This hook maintains that errors.__init__.py and testing.rst are in-sync. +Therefore, the exception or warning should be defined or imported in +errors.__init__.py. Ideally, the exception or warning is defined unless +there's special reason to import it. -Print the exception/warning that do not follow convention. +Prints the exception/warning that do not follow this convention. Usage:: -As pre-commit hook (recommended): +As a pre-commit hook: pre-commit run validate-errors-locations --all-files """ from __future__ import annotations import argparse import ast +import pathlib import sys from typing import Sequence +API_PATH = pathlib.Path("doc/source/reference/testing.rst").resolve() ERROR_MESSAGE = ( "{path}:{lineno}:{col_offset}: {exception_name}: " "Please don't place exceptions or warnings outside of pandas/errors/__init__.py or " @@ -70,19 +79,19 @@ "Warning", } -permisable_exception_warning_list = [ - "LossySetitemError", - "NoBufferPresent", - "InvalidComparison", - "NotThisMethod", - "OptionError", - "InvalidVersion", -] + +def get_warnings_and_exceptions_from_api_path() -> set[str]: + with open(API_PATH) as f: + doc_errors = { + line.split(".")[1].strip() for line in f.readlines() if "errors" in line + } + return doc_errors class Visitor(ast.NodeVisitor): - def __init__(self, path: str) -> None: + def __init__(self, path: str, exception_set: set[str]) -> None: self.path = path + self.exception_set = exception_set def visit_ClassDef(self, node): classes = {getattr(n, "id", None) for n in node.bases} @@ -90,7 +99,7 @@ def visit_ClassDef(self, node): if ( classes and classes.issubset(exception_warning_list) - and node.name not in permisable_exception_warning_list + and node.name not in self.exception_set ): msg = ERROR_MESSAGE.format( path=self.path, @@ -102,9 +111,11 @@ def visit_ClassDef(self, node): sys.exit(1) -def validate_exception_and_warning_placement(file_path: str, file_content: str): +def validate_exception_and_warning_placement( + file_path: str, file_content: str, errors: set[str] +): tree = ast.parse(file_content) - visitor = Visitor(file_path) + visitor = Visitor(file_path, errors) visitor.visit(tree) @@ -113,10 +124,12 @@ def main(argv: Sequence[str] | None = None) -> None: parser.add_argument("paths", nargs="*") args = parser.parse_args(argv) + error_set = get_warnings_and_exceptions_from_api_path() + for path in args.paths: with open(path, encoding="utf-8") as fd: content = fd.read() - validate_exception_and_warning_placement(path, content) + validate_exception_and_warning_placement(path, content, error_set) if __name__ == "__main__": From 4e89d88a189c99f3c416b4bf561b32c037740bef Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Tue, 6 Sep 2022 15:42:14 -0400 Subject: [PATCH 6/9] ENH: update whatsnew rst --- doc/source/whatsnew/v1.5.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index c479c59082464..6a51750cd6066 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -317,7 +317,7 @@ Other enhancements - A :class:`errors.PerformanceWarning` is now thrown when using ``string[pyarrow]`` dtype with methods that don't dispatch to ``pyarrow.compute`` methods (:issue:`42613`) - Added ``numeric_only`` argument to :meth:`Resampler.sum`, :meth:`Resampler.prod`, :meth:`Resampler.min`, :meth:`Resampler.max`, :meth:`Resampler.first`, and :meth:`Resampler.last` (:issue:`46442`) - ``times`` argument in :class:`.ExponentialMovingWindow` now accepts ``np.timedelta64`` (:issue:`47003`) -- :class:`.DataError`, :class:`.SpecificationError`, :class:`.SettingWithCopyError`, :class:`.SettingWithCopyWarning`, :class:`.NumExprClobberingError`, :class:`.UndefinedVariableError`, :class:`.IndexingError`, :class:`.PyperclipException`, :class:`.PyperclipWindowsException`, :class:`.CSSWarning`, :class:`.PossibleDataLossError`, :class:`.ClosedFileError`, :class:`.IncompatibilityWarning`, :class:`.AttributeConflictWarning`, :class:`.DatabaseError`, :class:`.PossiblePrecisionLoss`, :class:`.ValueLabelTypeMismatch`, :class:`.InvalidColumnName`, and :class:`.CategoricalConversionWarning` are now exposed in ``pandas.errors`` (:issue:`27656`) +- :class:`.DataError`, :class:`.SpecificationError`, :class:`.SettingWithCopyError`, :class:`.SettingWithCopyWarning`, :class:`.NumExprClobberingError`, :class:`.UndefinedVariableError`, :class:`.IndexingError`, :class:`.PyperclipException`, :class:`.PyperclipWindowsException`, :class:`.CSSWarning`, :class:`.PossibleDataLossError`, :class:`.ClosedFileError`, :class:`.IncompatibilityWarning`, :class:`.AttributeConflictWarning`, :class:`.DatabaseError`, :class:`.PossiblePrecisionLoss`, :class:`.ValueLabelTypeMismatch`, :class:`.InvalidColumnName`, :class:`.CategoricalConversionWarning`, :class:`.InvalidComparison`, :class:`.InvalidVersion`, :class:`.LossySetitemError`, :class:`.NoBufferPresent`, and :class:`.NotThisMethod` are now exposed in ``pandas.errors`` (:issue:`27656`) - Added ``check_like`` argument to :func:`testing.assert_series_equal` (:issue:`47247`) - Add support for :meth:`.GroupBy.ohlc` for extension array dtypes (:issue:`37493`) - Allow reading compressed SAS files with :func:`read_sas` (e.g., ``.sas7bdat.gz`` files) From 323ffd71ec4ba817f39e1bea5be763f25cd7a1b1 Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Tue, 13 Sep 2022 00:54:55 -0400 Subject: [PATCH 7/9] ENH: apply feedback changes --- doc/source/whatsnew/v1.5.0.rst | 2 +- doc/source/whatsnew/v1.6.0.rst | 2 +- .../tests/test_validate_exception_location.py | 25 ++++++--------- scripts/validate_exception_location.py | 32 +++++++++---------- 4 files changed, 27 insertions(+), 34 deletions(-) diff --git a/doc/source/whatsnew/v1.5.0.rst b/doc/source/whatsnew/v1.5.0.rst index 6a51750cd6066..c479c59082464 100644 --- a/doc/source/whatsnew/v1.5.0.rst +++ b/doc/source/whatsnew/v1.5.0.rst @@ -317,7 +317,7 @@ Other enhancements - A :class:`errors.PerformanceWarning` is now thrown when using ``string[pyarrow]`` dtype with methods that don't dispatch to ``pyarrow.compute`` methods (:issue:`42613`) - Added ``numeric_only`` argument to :meth:`Resampler.sum`, :meth:`Resampler.prod`, :meth:`Resampler.min`, :meth:`Resampler.max`, :meth:`Resampler.first`, and :meth:`Resampler.last` (:issue:`46442`) - ``times`` argument in :class:`.ExponentialMovingWindow` now accepts ``np.timedelta64`` (:issue:`47003`) -- :class:`.DataError`, :class:`.SpecificationError`, :class:`.SettingWithCopyError`, :class:`.SettingWithCopyWarning`, :class:`.NumExprClobberingError`, :class:`.UndefinedVariableError`, :class:`.IndexingError`, :class:`.PyperclipException`, :class:`.PyperclipWindowsException`, :class:`.CSSWarning`, :class:`.PossibleDataLossError`, :class:`.ClosedFileError`, :class:`.IncompatibilityWarning`, :class:`.AttributeConflictWarning`, :class:`.DatabaseError`, :class:`.PossiblePrecisionLoss`, :class:`.ValueLabelTypeMismatch`, :class:`.InvalidColumnName`, :class:`.CategoricalConversionWarning`, :class:`.InvalidComparison`, :class:`.InvalidVersion`, :class:`.LossySetitemError`, :class:`.NoBufferPresent`, and :class:`.NotThisMethod` are now exposed in ``pandas.errors`` (:issue:`27656`) +- :class:`.DataError`, :class:`.SpecificationError`, :class:`.SettingWithCopyError`, :class:`.SettingWithCopyWarning`, :class:`.NumExprClobberingError`, :class:`.UndefinedVariableError`, :class:`.IndexingError`, :class:`.PyperclipException`, :class:`.PyperclipWindowsException`, :class:`.CSSWarning`, :class:`.PossibleDataLossError`, :class:`.ClosedFileError`, :class:`.IncompatibilityWarning`, :class:`.AttributeConflictWarning`, :class:`.DatabaseError`, :class:`.PossiblePrecisionLoss`, :class:`.ValueLabelTypeMismatch`, :class:`.InvalidColumnName`, and :class:`.CategoricalConversionWarning` are now exposed in ``pandas.errors`` (:issue:`27656`) - Added ``check_like`` argument to :func:`testing.assert_series_equal` (:issue:`47247`) - Add support for :meth:`.GroupBy.ohlc` for extension array dtypes (:issue:`37493`) - Allow reading compressed SAS files with :func:`read_sas` (e.g., ``.sas7bdat.gz`` files) diff --git a/doc/source/whatsnew/v1.6.0.rst b/doc/source/whatsnew/v1.6.0.rst index 50f7104fb59c1..c852992df3933 100644 --- a/doc/source/whatsnew/v1.6.0.rst +++ b/doc/source/whatsnew/v1.6.0.rst @@ -29,7 +29,7 @@ enhancement2 Other enhancements ^^^^^^^^^^^^^^^^^^ - :meth:`Series.add_suffix`, :meth:`DataFrame.add_suffix`, :meth:`Series.add_prefix` and :meth:`DataFrame.add_prefix` support an ``axis`` argument. If ``axis`` is set, the default behaviour of which axis to consider can be overwritten (:issue:`47819`) -- +- :class:`.CategoricalConversionWarning`, :class:`.InvalidComparison`, :class:`.InvalidVersion`, :class:`.LossySetitemError`, :class:`.NoBufferPresent`, and :class:`.NotThisMethod` are now exposed in ``pandas.errors`` (:issue:`27656`) .. --------------------------------------------------------------------------- .. _whatsnew_160.notable_bug_fixes: diff --git a/scripts/tests/test_validate_exception_location.py b/scripts/tests/test_validate_exception_location.py index 1294d72743c61..9d493ee04d1c2 100644 --- a/scripts/tests/test_validate_exception_location.py +++ b/scripts/tests/test_validate_exception_location.py @@ -1,6 +1,9 @@ import pytest -from scripts.validate_exception_location import validate_exception_and_warning_placement +from scripts.validate_exception_location import ( + ERROR_MESSAGE, + validate_exception_and_warning_placement, +) PATH = "t.py" @@ -22,34 +25,26 @@ class {custom_name}({error_type}): """ + # Test with various python-defined exceptions to ensure they are all flagged. -testdata = [ - "Exception", - "ValueError", - "Warning", - "UserWarning", -] +@pytest.fixture(params=["Exception", "ValueError", "Warning", "UserWarning"]) +def error_type(request): + return request.param -@pytest.mark.parametrize("error_type", testdata) def test_class_that_inherits_an_exception_and_is_not_in_the_testing_rst_is_flagged( capsys, error_type ): content = TEST_CODE.format( custom_name=CUSTOM_EXCEPTION_NOT_IN_TESTING_RST, error_type=error_type ) - result_msg = ( - "t.py:8:0: {exception_name}: Please don't place exceptions or " - "warnings outside of pandas/errors/__init__.py or " - "pandas/_libs\n".format(exception_name=CUSTOM_EXCEPTION_NOT_IN_TESTING_RST) - ) + expected_msg = ERROR_MESSAGE.format(errors=CUSTOM_EXCEPTION_NOT_IN_TESTING_RST) with pytest.raises(SystemExit, match=None): validate_exception_and_warning_placement(PATH, content, ERRORS_IN_TESTING_RST) - expected_msg, _ = capsys.readouterr() + result_msg, _ = capsys.readouterr() assert result_msg == expected_msg -@pytest.mark.parametrize("error_type", testdata) def test_class_that_inherits_an_exception_but_is_in_the_testing_rst_is_not_flagged( capsys, error_type ): diff --git a/scripts/validate_exception_location.py b/scripts/validate_exception_location.py index 9da6056048444..62cf0f514dc1f 100644 --- a/scripts/validate_exception_location.py +++ b/scripts/validate_exception_location.py @@ -1,5 +1,5 @@ """ -Validate that the exceptions and warnings are in approrirate places. +Validate that the exceptions and warnings are in appropriate places. Checks for classes that inherit a python exception and warning and flags them, unless they are exempted from checking. Exempt meaning @@ -28,9 +28,10 @@ API_PATH = pathlib.Path("doc/source/reference/testing.rst").resolve() ERROR_MESSAGE = ( - "{path}:{lineno}:{col_offset}: {exception_name}: " - "Please don't place exceptions or warnings outside of pandas/errors/__init__.py or " - "pandas/_libs\n" + "The following exception(s) and/or warning(s): {errors} exist(s) outside of " + "pandas/errors/__init__.py. Please either define them in " + "pandas/errors/__init__.py. Or, if not possible then import them in " + "pandas/errors/__init__.py.\n" ) exception_warning_list = { "ArithmeticError", @@ -92,23 +93,13 @@ class Visitor(ast.NodeVisitor): def __init__(self, path: str, exception_set: set[str]) -> None: self.path = path self.exception_set = exception_set + self.possible_exceptions = set() def visit_ClassDef(self, node): classes = {getattr(n, "id", None) for n in node.bases} - if ( - classes - and classes.issubset(exception_warning_list) - and node.name not in self.exception_set - ): - msg = ERROR_MESSAGE.format( - path=self.path, - lineno=node.lineno, - col_offset=node.col_offset, - exception_name=node.name, - ) - sys.stdout.write(msg) - sys.exit(1) + if classes and classes.issubset(exception_warning_list): + self.possible_exceptions.add(node.name) def validate_exception_and_warning_placement( @@ -118,6 +109,13 @@ def validate_exception_and_warning_placement( visitor = Visitor(file_path, errors) visitor.visit(tree) + misplaced_exceptions = visitor.possible_exceptions.difference(errors) + + if misplaced_exceptions: + msg = ERROR_MESSAGE.format(errors=", ".join(misplaced_exceptions)) + sys.stdout.write(msg) + sys.exit(1) + def main(argv: Sequence[str] | None = None) -> None: parser = argparse.ArgumentParser() From b9ae6149d225e3037e279c22a825a9375a9aa08a Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Tue, 13 Sep 2022 17:46:59 -0400 Subject: [PATCH 8/9] ENH: refactor to remove exception_warning_list and ignore _version.py --- .pre-commit-config.yaml | 2 +- doc/source/reference/testing.rst | 1 - doc/source/whatsnew/v1.6.0.rst | 2 +- pandas/errors/__init__.py | 1 - scripts/validate_exception_location.py | 75 ++++++++------------------ 5 files changed, 25 insertions(+), 56 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eee9a55bcb309..fcff374e20dea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -242,7 +242,7 @@ repos: entry: python scripts/validate_exception_location.py language: python files: ^pandas/ - exclude: ^(pandas/_libs/|pandas/tests/|pandas/errors/__init__.py$) + exclude: ^(pandas/_libs/|pandas/tests/|pandas/errors/__init__.py$|pandas/_version.py) types: [python] - id: flake8-pyi name: flake8-pyi diff --git a/doc/source/reference/testing.rst b/doc/source/reference/testing.rst index ca13c85eb7289..07624e87d82e0 100644 --- a/doc/source/reference/testing.rst +++ b/doc/source/reference/testing.rst @@ -45,7 +45,6 @@ Exceptions and warnings errors.LossySetitemError errors.MergeError errors.NoBufferPresent - errors.NotThisMethod errors.NullFrequencyError errors.NumbaUtilError errors.NumExprClobberingError diff --git a/doc/source/whatsnew/v1.6.0.rst b/doc/source/whatsnew/v1.6.0.rst index 39a7c8433b53d..96f13dcf30970 100644 --- a/doc/source/whatsnew/v1.6.0.rst +++ b/doc/source/whatsnew/v1.6.0.rst @@ -31,7 +31,7 @@ Other enhancements - :meth:`.GroupBy.quantile` now preserving nullable dtypes instead of casting to numpy dtypes (:issue:`37493`) - :meth:`Series.add_suffix`, :meth:`DataFrame.add_suffix`, :meth:`Series.add_prefix` and :meth:`DataFrame.add_prefix` support an ``axis`` argument. If ``axis`` is set, the default behaviour of which axis to consider can be overwritten (:issue:`47819`) - :func:`assert_frame_equal` now shows the first element where the DataFrames differ, analogously to ``pytest``'s output (:issue:`47910`) -- :class:`.CategoricalConversionWarning`, :class:`.InvalidComparison`, :class:`.InvalidVersion`, :class:`.LossySetitemError`, :class:`.NoBufferPresent`, and :class:`.NotThisMethod` are now exposed in ``pandas.errors`` (:issue:`27656`) +- :class:`.CategoricalConversionWarning`, :class:`.InvalidComparison`, :class:`.InvalidVersion`, :class:`.LossySetitemError`, and :class:`.NoBufferPresent` are now exposed in ``pandas.errors`` (:issue:`27656`) - .. --------------------------------------------------------------------------- diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index f0bcf468b6f1a..eac0a0e6630e9 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -12,7 +12,6 @@ OutOfBoundsTimedelta, ) -from pandas._version import NotThisMethod from pandas.util.version import InvalidVersion diff --git a/scripts/validate_exception_location.py b/scripts/validate_exception_location.py index 62cf0f514dc1f..ebbe6c95a3ec9 100644 --- a/scripts/validate_exception_location.py +++ b/scripts/validate_exception_location.py @@ -33,52 +33,6 @@ "pandas/errors/__init__.py. Or, if not possible then import them in " "pandas/errors/__init__.py.\n" ) -exception_warning_list = { - "ArithmeticError", - "AssertionError", - "AttributeError", - "EOFError", - "Exception", - "FloatingPointError", - "GeneratorExit", - "ImportError", - "IndentationError", - "IndexError", - "KeyboardInterrupt", - "KeyError", - "LookupError", - "MemoryError", - "NameError", - "NotImplementedError", - "OSError", - "OverflowError", - "ReferenceError", - "RuntimeError", - "StopIteration", - "SyntaxError", - "SystemError", - "SystemExit", - "TabError", - "TypeError", - "UnboundLocalError", - "UnicodeDecodeError", - "UnicodeEncodeError", - "UnicodeError", - "UnicodeTranslateError", - "ValueError", - "ZeroDivisionError", - "BytesWarning", - "DeprecationWarning", - "FutureWarning", - "ImportWarning", - "PendingDeprecationWarning", - "ResourceWarning", - "RuntimeWarning", - "SyntaxWarning", - "UnicodeWarning", - "UserWarning", - "Warning", -} def get_warnings_and_exceptions_from_api_path() -> set[str]: @@ -93,13 +47,27 @@ class Visitor(ast.NodeVisitor): def __init__(self, path: str, exception_set: set[str]) -> None: self.path = path self.exception_set = exception_set - self.possible_exceptions = set() + self.found_exceptions = set() - def visit_ClassDef(self, node): - classes = {getattr(n, "id", None) for n in node.bases} + def visit_ClassDef(self, node) -> None: + def is_an_exception_subclass(base_id: str) -> bool: + return ( + base_id == "Exception" + or base_id.endswith("Warning") + or base_id.endswith("Error") + ) - if classes and classes.issubset(exception_warning_list): - self.possible_exceptions.add(node.name) + exception_classes = [] + + # Go through the class's bases and check if they are an Exception or Warning. + for base in node.bases: + base_id = getattr(base, "id", None) + if base_id and is_an_exception_subclass(base_id): + exception_classes.append(base_id) + + # The class subclassed an Exception or Warning so add it to the list. + if exception_classes: + self.found_exceptions.add(node.name) def validate_exception_and_warning_placement( @@ -109,8 +77,11 @@ def validate_exception_and_warning_placement( visitor = Visitor(file_path, errors) visitor.visit(tree) - misplaced_exceptions = visitor.possible_exceptions.difference(errors) + misplaced_exceptions = visitor.found_exceptions.difference(errors) + # If misplaced_exceptions isn't an empty list then there exists + # pandas-defined Exception or Warnings outside of pandas/errors/__init__.py, so + # we should flag them. if misplaced_exceptions: msg = ERROR_MESSAGE.format(errors=", ".join(misplaced_exceptions)) sys.stdout.write(msg) From 82dbaa033bc3af854cb6a0363fe417f95ea8a1e3 Mon Sep 17 00:00:00 2001 From: Derek Sharp Date: Tue, 20 Sep 2022 10:17:59 -0400 Subject: [PATCH 9/9] ENH: remove NotThisMethod from tests and all --- pandas/errors/__init__.py | 1 - pandas/tests/test_errors.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pandas/errors/__init__.py b/pandas/errors/__init__.py index eac0a0e6630e9..3e4f116953cb3 100644 --- a/pandas/errors/__init__.py +++ b/pandas/errors/__init__.py @@ -577,7 +577,6 @@ class InvalidComparison(Exception): "LossySetitemError", "MergeError", "NoBufferPresent", - "NotThisMethod", "NullFrequencyError", "NumbaUtilError", "NumExprClobberingError", diff --git a/pandas/tests/test_errors.py b/pandas/tests/test_errors.py index 6e44d7c32b0cb..5dffee587adcb 100644 --- a/pandas/tests/test_errors.py +++ b/pandas/tests/test_errors.py @@ -27,7 +27,6 @@ "LossySetitemError", "MergeError", "NoBufferPresent", - "NotThisMethod", "NumExprClobberingError", "NumbaUtilError", "OptionError",