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:
Some more thoughts:
 - We could potentially treat parameter names like `ions` similar to how `ion` is already treated.
 - A related possibility would be to have `any_of`, `exclude`, and `require` parameters for `ParticleList`, so that we could do things like:
   ```python
   >>> ParticleList(["e-", "e+"], require="ion")
   InvalidIonError: ...
   ``` 
   The difficulty would be about whether or not we should have methods like `ParticleList.append`, etc. also enforce the categorization criteria.  This capability would add extra complication to code within `ParticleList`, so I'm hesitant to do this unless there end up being a couple of use cases that can't be handled by `@particle_input`.
 - This came up in the same discussion about #1986 that led to #2045.
Looking back at this, I should note that this does work as expected with:

```
@particle_input
def get_ion(ion: ParticleLike):
    return ion

@particle_input
def get_isotope(isotope: ParticleLike):
    return isotope
```

In the above examples, `@particle_input` infers from the parameter name what the categorization criteria should be, and raises exceptions as expected.

Resolving patch:
diff --git a/src/plasmapy/particles/decorators.py b/src/plasmapy/particles/decorators.py
--- 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
--- 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)