Repository: PlasmaPy/PlasmaPy
Pull request #1039 (2021-02-24 17:10:01)

Improve docstring for `Particle.is_category`
The purpose of this PR is to ~~procrastinate doing other things~~ make the docstring for `Particle.is_category` consistent with the numpydoc standard.

codecov[bot]: # [Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039?src=pr&el=h1) Report
> Merging [#1039](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039?src=pr&el=desc) (a02dc83) into [master](https://codecov.io/gh/PlasmaPy/PlasmaPy/commit/8ca6dbdbc43431f4dd619975507b30c9c888b658?el=desc) (8ca6dbd) will **decrease** coverage by `0.59%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039/graphs/tree.svg?width=650&height=150&src=pr&token=LzZ5mnXVad)](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1039      +/-   ##
==========================================
- Coverage   96.86%   96.26%   -0.60%     
==========================================
  Files          70       70              
  Lines        6856     6856              
==========================================
- Hits         6641     6600      -41     
- Misses        215      256      +41     
```


| [Impacted Files](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [plasmapy/particles/particle\_class.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039/diff?src=pr&el=tree#diff-cGxhc21hcHkvcGFydGljbGVzL3BhcnRpY2xlX2NsYXNzLnB5) | `98.74% <ø> (ø)` | |
| [plasmapy/simulation/particletracker.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039/diff?src=pr&el=tree#diff-cGxhc21hcHkvc2ltdWxhdGlvbi9wYXJ0aWNsZXRyYWNrZXIucHk=) | `36.20% <0.00%> (-62.07%)` | :arrow_down: |
| [plasmapy/plasma/sources/plasma3d.py](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039/diff?src=pr&el=tree#diff-cGxhc21hcHkvcGxhc21hL3NvdXJjZXMvcGxhc21hM2QucHk=) | `93.75% <0.00%> (-6.25%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039?src=pr&el=footer). Last update [8ca6dbd...a02dc83](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1039?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

StanczakDominik: I literally just looked at https://github.com/PlasmaPy/PlasmaPy/settings/interaction_limits to check whether I can't stop you for your own good.
namurphy: > I literally just looked at https://github.com/PlasmaPy/PlasmaPy/settings/interaction_limits to check whether I can't stop you for your own good.

I don't know.  My ratio of (number of times listening to _Bohemian Rhapsody_) to (number of pull requests) is well over 3:1 over the last few days, so I'm having fun and doing pretty well apart from getting really sad about Freddie Mercury every once in a while.  

In any case, thank you for the rapid reviews!
StanczakDominik: That gives me an idea...

Patch:
diff --git a/changelog/1039.doc.rst b/changelog/1039.doc.rst
new file mode 100644
index 00000000..8d670b5a
--- /dev/null
+++ b/changelog/1039.doc.rst
@@ -0,0 +1,2 @@
+Put the docstring for `plasmapy.particles.Particle.is_category` into
+numpydoc format.
diff --git a/changelog/1039.trivial.rst b/changelog/1039.trivial.rst
new file mode 100644
index 00000000..205ef135
--- /dev/null
+++ b/changelog/1039.trivial.rst
@@ -0,0 +1,2 @@
+A set containing all valid particle categories may now be accessed via
+`plasmapy.particles.Particle.is_category.valid_categories`.
diff --git a/plasmapy/particles/particle_class.py b/plasmapy/particles/particle_class.py
index 557f8230..9c37f05f 100644
--- a/plasmapy/particles/particle_class.py
+++ b/plasmapy/particles/particle_class.py
@@ -21,7 +21,7 @@
 from collections import defaultdict, namedtuple
 from datetime import datetime
 from numbers import Integral, Real
-from typing import List, Optional, Set, Tuple, Union
+from typing import Iterable, List, Optional, Set, Tuple, Union
 
 from plasmapy.particles.elements import _Elements, _PeriodicTable
 from plasmapy.particles.exceptions import (
@@ -1453,41 +1453,86 @@ def categories(self) -> Set[str]:
     def is_category(
         self,
         *category_tuple,
-        require: Union[str, Set, Tuple, List] = None,
-        any_of: Union[str, Set, Tuple, List] = None,
-        exclude: Union[str, Set, Tuple, List] = None,
+        require: Union[str, Iterable[str]] = None,
+        any_of: Union[str, Iterable[str]] = None,
+        exclude: Union[str, Iterable[str]] = None,
     ) -> bool:
         """
         Determine if the particle meets categorization criteria.
 
-        Return `True` if the particle is in all of the inputted
-        categories, and `False` the particle is not.
+        Return `True` if the particle is consistent with the provided
+        categories, and `False` otherwise.
 
+        Parameters
+        ----------
+        *category_tuple
+            Required categories in the form of one or more `str` objects
+            or an iterable.
+
+        require : `str` or iterable providing `str` objects, optional, keyword-only
+            One or more categories.  This method will return `False` if
+            the `Particle` does not belong to all of these categories.
+
+        any_of : `str` or iterable providing `str` objects, optional, keyword-only
+            One or more categories. This method will return `False` if
+            the `Particle` does not belong to at least one of these
+            categories.
+
+        exclude : `str` or iterable providing `str` objects, optional, keyword-only
+            One or more categories.  This method will return `False` if
+            the `Particle` belongs to any of these categories.
+
+        Notes
+        -----
+        A `set` containing all valid categories may be accessed by the
+        ``valid_categories`` attribute of `~Particle.is_category`.
+
+        Examples
+        -----
         Required categories may be entered as positional arguments,
         including as a `list`, `set`, or `tuple` of required categories.
-        These may also be included using the ``require`` keyword
-        argument.  This method will return `False` if the particle is
-        not in all of the required categories.
 
-        If categories are inputted using the ``any_of`` keyword
-        argument, then this method will return `False` if the particle
-        is not of any of the categories in ``any_of``.
+        >>> electron = Particle("e-")
+        >>> electron.is_category("lepton")
+        True
+        >>> electron.is_category("lepton", "baryon")
+        False
+        >>> electron.is_category(["fermion", "matter"])
+        True
 
-        If the ``exclude`` keyword is set, then this method will return
-        `False` if the particle is in any of the excluded categories,
-        whether or not the particle matches the other criteria.
+        Required arguments may also be provided using the ``require``
+        keyword argument.
 
-        Examples
-        --------
-        >>> Particle('e-').is_category('lepton')
+        >>> electron.is_category(require="lepton")
         True
-        >>> Particle('p+').is_category('baryon', exclude='charged')
+        >>> electron.is_category(require=["lepton", "baryon"])
         False
-        >>> Particle('n').is_category({'matter', 'baryon'}, exclude={'charged'})
+
+        This method will return `False` if the particle does not belong
+        to at least one of the categories provided with the ``any_of``
+        keyword argument.
+
+        >>> electron.is_category(any_of=["lepton", "baryon"])
         True
-        >>> Particle('mu+').is_category('antilepton', exclude='baryon')
+        >>> electron.is_category(any_of=("noble gas", "lanthanide", "halogen"))
+        False
+
+        This method will return `False` if the particle belongs to any
+        of the categories provided in the ``exclude`` keyword argument.
+
+        >>> electron.is_category(exclude="baryon")
         True
+        >>> electron.is_category(exclude={"lepton", "baryon"})
+        False
 
+        The ``require``, ``any_of``, and ``exclude`` keywords may be
+        combined.  If the particle matches all of the provided criteria,
+        then `~Particle.is_category` will return `True`.
+
+        >>> electron.is_category(
+        ...     require="fermion", any_of={'lepton', 'baryon'}, exclude='charged',
+        ... )
+        False
         """
 
         def become_set(arg: Union[str, Set, Tuple, List]) -> Set[str]:
@@ -1510,13 +1555,9 @@ def become_set(arg: Union[str, Set, Tuple, List]) -> Set[str]:
             )
 
         require = become_set(category_tuple) if category_tuple else become_set(require)
-
         exclude = become_set(exclude)
         any_of = become_set(any_of)
 
-        if not require and not exclude and not any_of:
-            return _valid_categories
-
         invalid_categories = (require | exclude | any_of) - _valid_categories
 
         duplicate_categories = require & exclude | exclude & any_of | require & any_of
@@ -1738,6 +1779,10 @@ def recombine(self, n: Integral = 1, inplace=False):
             return Particle(base_particle, Z=new_integer_charge)
 
 
+Particle.is_category.valid_categories = _valid_categories
+"""All valid particle categories."""
+
+
 class DimensionlessParticle(AbstractParticle):
     """
     A class to represent dimensionless custom particles.


Test patch:
diff --git a/plasmapy/particles/tests/test_particle_class.py b/plasmapy/particles/tests/test_particle_class.py
index 3a0fde47..1301cecd 100644
--- a/plasmapy/particles/tests/test_particle_class.py
+++ b/plasmapy/particles/tests/test_particle_class.py
@@ -1344,3 +1344,20 @@ def test_particle_to_json_file(cls, kwargs, expected_repr):
         f"expected_repr = {expected_repr['__init__']}.\n\n"
         f"json_repr: {test_dict['__init__']}"
     )
+
+
+def test_particle_is_category_valid_categories():
+    """Test the location where valid categories may be accessed."""
+    assert hasattr(Particle.is_category, "valid_categories")
+    some_valid_categories = {
+        "lepton",
+        "fermion",
+        "matter",
+        "nonmetal",
+        "electron",
+        "ion",
+        "isotope",
+        "charged",
+        "uncharged",
+    }
+    assert some_valid_categories.issubset(Particle.is_category.valid_categories)