Repository: PlasmaPy/PlasmaPy

Issue:
Create physics exceptions and warnings
Several of the exceptions and warnings that come up in PlasmaPy result from when we try to violate the laws of physics.  Most of the time these violations cause a `UserWarning` or raise a `ValueError`, but these are pretty generic and don't give much insight into the cause of the problem.  To help with exception handling, I propose that we create new exceptions and warnings, such as `PhysicsError` and `PhysicsWarning`.  We could additionally be more specific and also have `RelativityError` and/or `RelativityWarning`.  One possibility would be to put these in a new file called `plasmapy/utils/exceptions.py`.

This would be a good first contribution for someone who is somewhat familiar with Python.  Here's documentation on [user-defined exceptions](https://docs.python.org/3/tutorial/errors.html#user-defined-exceptions.  Thank you!



Discussion:
I think I could give this a shot.  I was actually meaning to ask about the warnings situation, right now whenever I calculate a maybe-relativity-is-important parameter it gives me a UserWarning that will stop execution if I don't catch it. It seems a bit aggressive, would we rather generate / display these warnings in a non-halting way?
Good catch!  A while ago I had put in some `raise UserWarning` statements.  We decided afterwards change them to use `warnings.warn` because of the problem you're bringing up, but it looks like we missed a few in `plasmapy/utils/checks.py` and maybe elsewhere.  That would be a good thing to fix too!

Resolving patch:
diff --git a/plasmapy/utils/exceptions.py b/plasmapy/utils/exceptions.py
--- a/plasmapy/utils/exceptions.py
+++ b/plasmapy/utils/exceptions.py
@@ -18,14 +18,51 @@ class PlasmaPyError(Exception):
     already knows how to handle a ValueError, it won't need any specific
     modification.
     """
+    pass
 
 
 class PhysicsError(PlasmaPyError, ValueError):
     """Error for use of a physics value outside PlasmaPy theoretical bounds"""
+    pass
 
 
 class RelativityError(PhysicsError):
     """Error for use of a speed greater than or equal to the speed of light"""
+    pass
+
+
+class AtomicError(PlasmaPyError):
+    """Error for use by an atomic subpackage"""
+    pass
+
+
+class MissingAtomicDataError(AtomicError):
+    """Error for use when atomic data is missing."""
+    pass
+
+
+class NoChargeInfoError(AtomicError):
+    """Error for use when charge information is needed but missing."""
+
+
+class IonError(NoChargeInfoError):
+    """Error for use when an ion is invalid."""
+    pass
+
+
+class IsotopeError(AtomicError):
+    """Error for use when an isotope is invalid."""
+    pass
+
+
+class ElementError(IsotopeError, IonError):
+    """Error for use when an element is invalid."""
+    pass
+
+
+class ParticleError(ElementError):
+    """Error for use when a particle is invalid."""
+    pass
 
 
 # ----------
@@ -41,11 +78,24 @@ class PlasmaPyWarning(Warning):
     Warnings should be issued using warnings.warn, which will not break
     execution if unhandled.
     """
+    pass
 
 
 class PhysicsWarning(PlasmaPyWarning):
     """Warning for using a mildly worrisome physics value"""
+    pass
 
 
 class RelativityWarning(PhysicsWarning):
     """Warning for use of a speed quantity approaching the speed of light"""
+    pass
+
+
+class AtomicWarning(PlasmaPyWarning):
+    """Warnings for use in the atomic subpackage."""
+    pass
+
+
+class MissingAtomicDataWarning(AtomicWarning):
+    """Warning for use when atomic data is missing."""
+    pass


Test patch:
diff --git a/plasmapy/utils/tests/test_exceptions.py b/plasmapy/utils/tests/test_exceptions.py
new file mode 100644
--- /dev/null
+++ b/plasmapy/utils/tests/test_exceptions.py
@@ -0,0 +1,60 @@
+import pytest
+import warnings
+
+from ..exceptions import *
+
+plasmapy_exceptions = [
+    PlasmaPyError,
+    PhysicsError,
+    RelativityError,
+    AtomicError,
+    MissingAtomicDataError,
+    NoChargeInfoError,
+    IonError,
+    IsotopeError,
+    ElementError,
+    ParticleError,
+]
+
+
+@pytest.mark.parametrize("exception", plasmapy_exceptions)
+def test_exceptions(exception):
+    r"""Test that custom PlasmaPy exceptions can be raised with an
+    error message."""
+    with pytest.raises(exception, message=f"Problem raising {exception}"):
+        raise exception("What an exceptionally exceptional exception!")
+
+
+@pytest.mark.parametrize("exception", plasmapy_exceptions)
+def test_PlasmaPyError_subclassing(exception):
+    r"""Test that each custom PlasmaPy exception can be caught
+    as a PlasmaPyError."""
+    with pytest.raises(PlasmaPyError, message=(
+            f"Problem with subclassing of {exception}")):
+        raise exception("I'm sorry, Dave.  I'm afraid I can't do that.")
+
+
+plasmapy_warnings = [
+    PlasmaPyWarning,
+    PhysicsWarning,
+    RelativityWarning,
+    AtomicWarning,
+    MissingAtomicDataWarning,
+]
+
+
+@pytest.mark.parametrize("warning", plasmapy_warnings)
+def test_warnings(warning):
+    r"""Test that custom PlasmaPy warnings can be issued with a
+    warning message."""
+    with pytest.warns(warning, message=f"Problem issuing {warning}"):
+        warnings.warn("Coverage decreased (-0.00002%)", warning)
+
+
+@pytest.mark.parametrize("warning", plasmapy_warnings)
+def test_PlasmaPyWarning_subclassing(warning):
+    r"""Test that custom PlasmaPy warnings can all be caught
+    as a PlasmaPyWarning."""
+    with pytest.warns(PlasmaPyWarning, message=(
+            f"Problem with subclassing of {warning}")):
+        warnings.warn("Electrons are WEIRD.", warning)