Repository: PlasmaPy/PlasmaPy

Issue:
`@particle_input` does not apply categorization criteria when creating a `ParticleList`
### Bug description

The `require`, `exclude`, and `any_of` parameters of `@particle_input` do not get enforced when the decorated function gets passed a `ParticleList`.

### Expected outcome

When we use the `require`, `any_of`, and `exclude` parameters with `@particle_input`, we should make them work with `ParticleLike`.

```Python
@particle_input(require="ion", exclude={"isotope"})
def f(x: ParticleLike):
    return x
```


### Minimal complete verifiable example

```Python
>>> from plasmapy.particles import ParticleLike, particle_input
>>> 
>>> @particle_input(require="ion")
... def f(particle: ParticleLike):
...     return particle
...
>>> f(["e-", "e+"])  # should raise an error because these are not ions
ParticleList(['e-', 'e+'])
```


### Package versions

_No response_

### Additional context

When the decorated parameter is named `ion` (for example), `@particle_input` does indeed check that the list contains only ions. 

```pycon
>>> @particle_input
... def g(ion: ParticleLike):
...     return ion
...
>>> g(["e-", "e+"])
plasmapy.particles.exceptions.InvalidIonError: The argument ion = ParticleList(['e-', 'e+']) to g does not correspond to a valid ion.
```

However, there's a little singular vs. plural weirdness in the error message that we could fix.

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PlasmaPy/PlasmaPy/pull/2594?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.72%. Comparing base [(`20e60aa`)](https://app.codecov.io/gh/PlasmaPy/PlasmaPy/commit/20e60aa88f7dbb08bea5dc6fb2fd7926cd367905?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy) to head [(`34a7d72`)](https://app.codecov.io/gh/PlasmaPy/PlasmaPy/pull/2594?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy).
> Report is 76 commits behind head on main.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2594      +/-   ##
==========================================
- Coverage   96.93%   96.72%   -0.21%     
==========================================
  Files         104      104              
  Lines        9163     9371     +208     
==========================================
+ Hits         8882     9064     +182     
- Misses        281      307      +26     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PlasmaPy/PlasmaPy/pull/2594?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy).

namurphy: This PR is partially superseded by #2648.  I might close this and open a follow-up PR to add the tests from this PR.  Ultimately, I think #2648 will make this PR (or its follow-up) easier and cleaner.

Patch:
diff --git a/changelog/2594.bugfix.rst b/changelog/2594.bugfix.rst
new file mode 100644
index 00000000..53f1932f
--- /dev/null
+++ b/changelog/2594.bugfix.rst
@@ -0,0 +1,2 @@
+Fixed a bug in |particle_input| where particle categorization criteria
+had not been applied to arguments that became a |ParticleList|.
diff --git a/src/plasmapy/particles/decorators.py b/src/plasmapy/particles/decorators.py
index 300b95ef..94937a42 100644
--- a/src/plasmapy/particles/decorators.py
+++ b/src/plasmapy/particles/decorators.py
@@ -431,19 +431,11 @@ def verify_particle_categorization(
         --------
         ~plasmapy.particles.particle_class.Particle.is_category
         """
-        if isinstance(particle, ParticleList):
-            particle_in_category = particle.is_category(
-                require=self.require,
-                any_of=self.any_of,
-                exclude=self.exclude,
-                particlewise=True,
-            )
-        else:
-            particle_in_category = particle.is_category(
-                require=self.require,
-                any_of=self.any_of,
-                exclude=self.exclude,
-            )
+        particle_in_category = particle.is_category(
+            require=self.require,
+            any_of=self.any_of,
+            exclude=self.exclude,
+        )
         if not particle_in_category:
             errmsg = self.category_errmsg(
                 particle,
@@ -468,7 +460,7 @@ def verify_particle_name_criteria(
             and not np.isnan(particle.charge)
             and particle.mass.value > 0
         ):
-            return
+            return None
 
         name_categorization_exception: list[
             tuple[str, dict[str, str | Iterable[str] | None], type]
@@ -964,7 +956,6 @@ def instance_method(self, particle: ParticleLike, B: u.Quantity[u.T]): ...
     ...     return isotope.mass_number
     >>> mass_number("D")
     2
-
     """
 
     # The following pattern comes from the docs for wrapt, and requires


Test patch:
diff --git a/tests/particles/test_decorators.py b/tests/particles/test_decorators.py
index 6063da3d..3d7cdc73 100644
--- a/tests/particles/test_decorators.py
+++ b/tests/particles/test_decorators.py
@@ -18,7 +18,7 @@
     ParticleError,
 )
 from plasmapy.particles.particle_class import CustomParticle, Particle, ParticleLike
-from plasmapy.particles.particle_collections import ParticleList
+from plasmapy.particles.particle_collections import ParticleList, ParticleListLike
 from plasmapy.utils.code_repr import call_string
 from plasmapy.utils.decorators.validators import validate_quantities
 from plasmapy.utils.exceptions import PlasmaPyDeprecationWarning
@@ -748,3 +748,31 @@ def function_with_pos_and_var_positional_arguments(
     expected = (a, args, Particle(particle))
     actual = function_with_pos_and_var_positional_arguments(a, *args, particle=particle)
     assert actual == expected
+
+
+@pytest.mark.parametrize(
+    ("criteria", "kwargs", "exception"),
+    [
+        ({"require": "ion"}, {"particle": ["p+", "He"]}, ParticleError),
+        ({"require": "isotope"}, {"particle": ["p+", "He"]}, ParticleError),
+        (
+            {"any_of": {"lepton", "neutrino"}},
+            {"particle": ["p+", "alpha"]},
+            ParticleError,
+        ),
+        ({"exclude": "lepton"}, {"particle": ["p+", "e-"]}, ParticleError),
+    ],
+)
+def test_particle_categorization_of_particle_lists(
+    criteria: dict[str, str | Iterable[str]],
+    kwargs: dict[str, ParticleListLike],
+    exception: Exception,
+) -> None:
+    @particle_input(**criteria)  # type: ignore[arg-type]
+    def get_particle(
+        particle: ParticleLike,
+    ) -> Particle | ParticleList | CustomParticle:
+        return particle  # type: ignore[return-value]
+
+    with pytest.raises(exception):  # type: ignore[call-overload]
+        get_particle(**kwargs)