Repository: PlasmaPy/PlasmaPy

Issue:
Test the consistency of requirements defined in multiple places
The problem described in #1276 is that our requirements are defined in multiple places.  While #1276 proposes to consolidate requirements in a single place, it's not clear if there is a reasonable way to do so.

An alternative to #1276 would be to keep the structure of the requirements as they are now, and create a test to make sure that the different places where requirements are defined are consistent with each other.  This would probably be more straightforward than consolidating the requirements into a single place, but there's a possibility that the tests that do these checks could end up being brittle.  This is probably one of those situations where we ought to do a tradeoff analysis, since I'm not sure what the best way to approach this would be.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1500?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy) Report
> Merging [#1500](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1500?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy) (c7e5c8b) into [main](https://codecov.io/gh/PlasmaPy/PlasmaPy/commit/5ecb7650c132ab053a436875f7f30798244d840f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy) (5ecb765) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##             main    #1500   +/-   ##
=======================================
  Coverage   97.06%   97.06%           
=======================================
  Files          82       82           
  Lines        7739     7739           
=======================================
  Hits         7512     7512           
  Misses        227      227           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1500?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1500?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy). Last update [5ecb765...c7e5c8b](https://codecov.io/gh/PlasmaPy/PlasmaPy/pull/1500?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PlasmaPy).

namurphy:  - [ ] Finish updating usage of `toml` to `tomli`, since they seem to have a different API.
namurphy: @rocco8773's idea regarding the pre-commit test failure — try putting a restriction on which version of `click` can be used

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 606f084c..4a68af3c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -79,3 +79,9 @@ repos:
   - id: pyupgrade
     args: [--keep-runtime-typing, --py38-plus]
     files: ^plasmapy/
+
+- repo: https://github.com/pre-commit/pygrep-hooks
+  rev: v1.9.0
+  hooks:
+  - id: rst-directive-colons
+  - id: rst-inline-touching-normal
diff --git a/changelog/1500.trivial.1.rst b/changelog/1500.trivial.1.rst
new file mode 100644
index 00000000..3cc787f4
--- /dev/null
+++ b/changelog/1500.trivial.1.rst
@@ -0,0 +1,3 @@
+Added tests to verify that the requirements given in the :file:`.txt`
+files in the :file:`requirements` directory are consistent with the
+requirements given in :file:`setup.cfg` and :file:`pyproject.toml`.
diff --git a/changelog/1500.trivial.2.rst b/changelog/1500.trivial.2.rst
new file mode 100644
index 00000000..57eb23a3
--- /dev/null
+++ b/changelog/1500.trivial.2.rst
@@ -0,0 +1,1 @@
+Added ``tomli`` to the ``tests`` category of requirements.
diff --git a/changelog/1633.removal.1.rst b/changelog/1633.removal.1.rst
new file mode 100644
index 00000000..b70995a5
--- /dev/null
+++ b/changelog/1633.removal.1.rst
@@ -0,0 +1,3 @@
+The ``integer_charge`` attributes of |Particle| and |IonicLevel| have
+been removed after being deprecated in ``v0.7.0``. Use the
+``charge_number`` attribute instead.
diff --git a/changelog/1633.removal.2.rst b/changelog/1633.removal.2.rst
new file mode 100644
index 00000000..abe1c473
--- /dev/null
+++ b/changelog/1633.removal.2.rst
@@ -0,0 +1,3 @@
+The ``plasmapy.particles.atomic.integer_charge`` function has been
+removed after being deprecated in ``v0.7.0``. Use
+`~plasmapy.particles.atomic.charge_number` instead.
diff --git a/changelog/1633.removal.3.rst b/changelog/1633.removal.3.rst
new file mode 100644
index 00000000..74b5428c
--- /dev/null
+++ b/changelog/1633.removal.3.rst
@@ -0,0 +1,4 @@
+The ``integer_charges`` attributes of |IonizationState| has been
+removed after being deprecated in ``v0.7.0``. Use
+`~plasmapy.particles.ionization_state.IonizationState.charge_numbers`
+instead.
diff --git a/changelog/1655.doc.rst b/changelog/1655.doc.rst
new file mode 100644
index 00000000..bda58559
--- /dev/null
+++ b/changelog/1655.doc.rst
@@ -0,0 +1,1 @@
+Added sections to the |coding guide| on units and particles.
diff --git a/changelog/1659.doc.rst b/changelog/1659.doc.rst
new file mode 100644
index 00000000..88dd3981
--- /dev/null
+++ b/changelog/1659.doc.rst
@@ -0,0 +1,1 @@
+Added sections to the |coding guide| on imports and requirements.
diff --git a/changelog/1667.doc.rst b/changelog/1667.doc.rst
new file mode 100644
index 00000000..2b371467
--- /dev/null
+++ b/changelog/1667.doc.rst
@@ -0,0 +1,2 @@
+Updated the section of the |documentation guide| with more detail on the
+"Parameters", "Raises", and "Warns" sections of docstrings.
diff --git a/docs/common_links.rst b/docs/common_links.rst
index 4cfc169c..d008cf5a 100644
--- a/docs/common_links.rst
+++ b/docs/common_links.rst
@@ -58,6 +58,12 @@
 
 .. |ParticleTracker| replace:: :class:`~plasmapy.simulation.particletracker.ParticleTracker`
 
+.. --------------
+.. plasmapy.utils
+.. --------------
+
+.. |validate_quantities| replace:: :func:`~plasmapy.utils.decorators.validators.validate_quantities`
+
 .. ------------------
 .. NumPy replacements
 .. ------------------
@@ -108,6 +114,7 @@
 .. _create an issue: https://github.com/PlasmaPy/PlasmaPy/issues/new/choose
 .. _CSS: https://www.w3schools.com:443/css
 .. _DOI: https://www.doi.org
+.. _equivalencies: https://docs.astropy.org/en/stable/units/equivalencies.html
 .. _flake8: https://flake8.pycqa.org/en/latest
 .. _git: https://git-scm.com
 .. _GitHub Actions: https://docs.github.com/en/actions
@@ -176,6 +183,9 @@
    nitpick_ignore_regex in docs/conf.py so that it doesn't get counted
    as an error in a nitpicky doc build (e.g., tox -e doc_build_nitpicky).
 
+.. _`astropy.units`: https://docs.astropy.org/en/stable/units/index.html
+.. |astropy.units| replace:: `astropy.units`
+
 .. _`docs/_static`: https://github.com/PlasmaPy/PlasmaPy/tree/main/docs/_static
 .. |docs/_static| replace:: :file:`docs/_static`
 
@@ -221,6 +231,18 @@
 .. _`.pre-commit-config.yaml`: https://github.com/PlasmaPy/PlasmaPy/blob/main/.pre-commit-config.yaml
 .. |.pre-commit-config.yaml| replace:: :file:`.pre-commit-config.yaml`
 
+.. _`pyproject.toml`: https://github.com/PlasmaPy/PlasmaPy/blob/main/pyproject.toml
+.. |pyproject.toml| replace:: :file:`pyproject.toml`
+
+.. _`requirements`: https://github.com/PlasmaPy/PlasmaPy/blob/main/requirements
+.. |requirements| replace:: :file:`requirements`
+
+.. _`requirements/build.txt`: https://github.com/PlasmaPy/PlasmaPy/blob/main/requirements/build.txt
+.. |requirements/build.txt| replace:: :file:`requirements/build.txt`
+
+.. _`requirements/environment.yml`: https://github.com/PlasmaPy/PlasmaPy/blob/main/requirements/environment.yml
+.. |requirements/environment.yml| replace:: :file:`requirements/environment.yml`
+
 .. _`setup.cfg`: https://github.com/PlasmaPy/PlasmaPy/blob/main/setup.cfg
 .. |setup.cfg| replace:: :file:`setup.cfg`
 
@@ -248,5 +270,8 @@
 .. _`sphinx-notfound-page`: https://sphinx-notfound-page.readthedocs.io
 .. |sphinx-notfound-page| replace:: `sphinx-notfound-page`
 
+.. _`tox.ini`: https://github.com/PlasmaPy/PlasmaPy/blob/main/tox.ini
+.. |tox.ini| replace:: :file:`tox.ini`
+
 .. _xarray: https://docs.xarray.dev
 .. |xarray| replace:: `xarray`
diff --git a/docs/contributing/coding_guide.rst b/docs/contributing/coding_guide.rst
index 45f47c5d..25b845a5 100644
--- a/docs/contributing/coding_guide.rst
+++ b/docs/contributing/coding_guide.rst
@@ -261,6 +261,92 @@ the code is supposed to be doing.
    can be done with :kbd:`Shift+F6` on Windows or Linux, and :kbd:`⇧F6`
    or :kbd:`⌥⌘R` on macOS.
 
+Imports
+=======
+
+* Use standard abbreviations for imported packages:
+
+  .. code-block:: python
+
+     import numpy as np
+     import astropy.units as u
+     import astropy.constants as const
+     import matplotlib.pyplot as plt
+     import pandas as pd
+
+* PlasmaPy uses isort_ to sort import statements via a |pre-commit|_
+  hook.
+
+* For infrequently used objects, import the package, subpackage, or
+  module rather than the individual code object. Including more of the
+  namespace provides contextual information that can make code easier to
+  read. For example, ``json.loads`` is more readable than using only
+  ``loads``.
+
+* For frequently used objects (e.g., |Particle|) and type hint
+  annotations (e.g., `~typing.Optional` and `~numbers.Real`), import the
+  object directly instead of importing the package, subpackage, or
+  module. Including more of the namespace would increase clutter and
+  decrease readability without providing commensurately more
+  information.
+
+* Use absolute imports (e.g., ``from plasmapy.particles import Particle``)
+  rather than relative imports (e.g., ``from ..particles import Particle``).
+
+* Do not use star imports (e.g., ``from package.subpackage import *``),
+  except in very limited situations.
+
+Requirements
+============
+
+* Package requirements are specified in multiple locations that need to
+  be updated simultaneously.
+
+  - The |requirements|_ directory contains multiple text files that
+    contain build, installation, testing, documentation, and extra
+    requirements.
+
+  - The ``build-system.requires`` section of |pyproject.toml|_ includes
+    the requirements for building PlasmaPy. This section must mirror
+    |requirements/build.txt|_.
+
+  - |setup.cfg|_ includes sections for the install, docs, tests, and
+    extra requirements that must mirror the corresponding files in
+    the |requirements|_ directory.
+
+  - |requirements/environment.yml|_ contains a Conda_ environment
+    for PlasmaPy.
+
+  - |tox.ini|_ contains a testing environment for the minimal
+    dependencies.
+
+* Each release of PlasmaPy should support all minor versions of
+  Python that have been released in the prior 42 months, and all minor
+  versions of NumPy_ that have been released in the last 24 months.
+  This schedule was proposed in `NumPy Enhancement Proposal 29`_ for
+  the scientific Python ecosystem, and has been adopted by upstream
+  packages such as NumPy_, matplotlib_, and Astropy_.
+
+  .. tip::
+
+     Tools like pyupgrade_ help automatically upgrade the code base to
+     the minimum supported version of Python for the next release.
+
+* In general, it is preferable to support minor releases of dependencies
+  from the last ≲ 24 months, unless there is a new feature in a
+  dependency that would be greatly beneficial for `plasmapy` development.
+
+* Do not set maximum requirements (e.g., ``numpy <= 1.22.3``) unless
+  absolutely necessary. Maximum requirements can lead to version
+  conflicts when installed alongside other packages. Instead, update
+  PlasmaPy to become compatible with the latest versions of its
+  dependencies. Similarly, do not require exact versions of packages
+  (e.g., ``scipy == 1.5.3``).
+
+* Minor versions of Python are generally released in October of each
+  year. However, it may take a few months before packages like NumPy_
+  and Numba_ become compatible with the newest minor version of Python_.
+
 .. _code-contribution:
 
 Branches, commits, and pull requests
@@ -494,56 +580,132 @@ of frustration.
 Units
 =====
 
-* Code within PlasmaPy must use SI units to minimize the chance of
-  ambiguity, and for consistency with the recognized international
-  standard.  Physical formulae and expressions should be in base SI
-  units.
+PlasmaPy uses |astropy.units|_ to assign physical units to values in the
+form of a |Quantity|.
+
+.. code-block:: pycon
+
+   >>> import astropy.units as u
+   >>> 5 * u.m / u.s
+   <Quantity 5. m / s>
+
+Using |astropy.units|_ improves compatibility with Python packages in
+adjacent fields such as astronomy and heliophysics. To get started with
+|astropy.units|_, check out this `example notebook on units`_.
+
+  .. caution::
+
+     Some `scipy` functions silently drop units when used on |Quantity|
+     instances.
+
+* Only SI units should be used within PlasmaPy, unless there is a strong
+  justification to do otherwise. Example notebooks may occasionally use
+  other unit systems to show the flexibility of |astropy.units|_.
+
+* Use operations between |Quantity| instances except when needed for
+  performance. To improve performance in |Quantity| operations, check
+  out `performance tips
+  <https://docs.astropy.org/en/stable/units/index.html#performance-tips>`__
+  for |astropy.units|_.
+
+* Use unit annotations with the |validate_quantities| decorator to
+  validate |Quantity| arguments and return values.
+
+  .. code-block:: python
+
+     from plasmapy.utils.decorators.validators import validate_quantities
+
+     @validate_quantities(
+        n={"can_be_negative": False},
+        validations_on_return={"equivalencies": u.dimensionless_angles()},
+     )
+     def inertial_length(n: u.m ** -3, ...) -> u.m:
+         ...
+
+  .. caution::
+
+     Recent versions of Astropy_ allow unit-aware |Quantity|
+     annotations such as ``u.Quantity[u.m]``. However, these annotations
+     are not yet compatible with |validate_quantities|.
+
+* Avoid using electron-volts as a unit of temperature within PlasmaPy
+  because it is defined as a unit of energy. However, functions in
+  `plasmapy.formulary` and elsewhere should accept temperatures in units
+  of electron-volts, which can be done using |validate_quantities|.
+
+* Non-standard unit conversions can be made using equivalencies_ such
+  as `~astropy.units.temperature_energy`.
+
+  .. code-block:: pycon
+
+     >>> (1 * u.eV).to(u.K, equivalencies=u.temperature_energy())
+     11604.518...
+
+* The names of SI units should not be capitalized except at the
+  beginning of a sentence, including when they are named after a person.
+  The sole exception is "degree Celsius".
+
+Particles
+=========
+
+The |Particle| class provides an object-oriented interface for accessing
+basic particle data. |Particle| accepts :term:`particle-like` inputs.
+
+.. code-block:: pycon
+
+   >>> from plasmapy.particles import Particle
+   >>> alpha = Particle("He-4 2+")
+   >>> alpha.mass
+   <Quantity 6.6446...e-27 kg>
+   >>> alpha.charge
+   <Quantity 3.20435...e-19 C>
+
+To get started with `plasmapy.particles`, check out this `example
+notebook on particles`_.
 
-  * Functions should not accept floats when an Astropy Quantity is
-    expected.  In particular, functions should not accept floats and
-    make the assumption that the value will be in SI units.
+* Avoid using implicit default particle assumptions for function
+  arguments (see issue :issue:`453`).
 
-  * A common convention among plasma physicists is to use
-    electron-volts (eV) as a unit of temperature.  Strictly speaking,
-    this unit corresponds not to temperature but is rather a measure
-    of the thermal energy per particle.  Code within PlasmaPy must use
-    the kelvin (K) as the unit of temperature to avoid unnecessary
-    ambiguity.
+* The |particle_input| decorator can automatically transform a
+  :term:`particle-like` :term:`argument` into a |Particle| instance when
+  the corresponding :term:`parameter` is decorated with |Particle|.
 
-* PlasmaPy uses the astropy.units package to give physical units to
-  values.
+  .. code-block:: python
+
+     from plasmapy.particles import particle_input, Particle
+
+     @particle_input
+     def get_particle(particle: Particle):
+          return particle
 
-  * All units packages available in Python presently have some
-    limitations, including incompatibility with some NumPy and SciPy
-    functions.  These limitations are due to issues within NumPy
-    itself.  Many of these limitations are being resolved, but require
-    upstream fixes.
+  Then if we use ``get_particle`` on something :term:`particle-like`,
+  then it will return the corresponding |Particle|.
+
+  .. code-block:: pycon
 
-* Dimensionless units may be used when appropriate, such as for
-  certain numerical simulations.  The conventions and normalizations
-  should be clearly described in docstrings.
+     >>> return_particle("p+")
+     Particle("p+")
+
+  The documentation for |particle_input| describes ways to ensure that
+  the particle meets certain categorization criteria.
 
 Equations and Physical Formulae
 ===============================
 
-* If a quantity has several names, then the function name should be
-  the one that provides the most physical insight into what the
-  quantity represents.  For example, ``gyrofrequency`` indicates
-  gyration, whereas ``Larmor_frequency`` indicates that this frequency
-  is somehow related to someone named Larmor.  Similarly, using
-  ``omega_ce`` as a function name will make the code less readable to
-  people who are unfamiliar with this particular notation.
-
 * Physical formulae should be inputted without first evaluating all of
-  the physical constants.  For example, the following line of code
+  the physical constants. For example, the following line of code
   obscures information about the physics being represented:
 
->>> omega_ce = 1.76e7*(B/u.G)*u.rad/u.s   # doctest: +SKIP
+  .. code-block:: pycon
+
+     >>> omega_ce = 1.76e7*(B/u.G)*u.rad/u.s  # doctest: +SKIP
 
   In contrast, the following line of code shows the exact formula
   which makes the code much more readable.
 
->>> omega_ce = (e * B) / (m_e * c)       # doctest: +SKIP
+  .. code-block:: pycon
+
+     >>> omega_ce = (e * B) / (m_e * c)  # doctest: +SKIP
 
   The origins of numerical coefficients in formulae should be
   documented.
@@ -553,24 +715,6 @@ Equations and Physical Formulae
   taking their first course in plasma physics while still being useful
   to experienced plasma physicists.
 
-* SI units that were named after a person should not be capitalized
-  except at the beginning of a sentence.
-
-* Some plasma parameters depend on more than one quantity with
-  the same units.  In the following line, it is difficult to discern which
-  is the electron temperature and which is the ion temperature.
-
-  >>> ion_sound_speed(1e6*u.K, 2e6*u.K)  # doctest: +SKIP
-
-  Remembering that "explicit is better than implicit", it is more
-  readable and less prone to errors to write:
-
-  >>> ion_sound_speed(T_i=1e6*u.K, T_e=2e6*u.K)    # doctest: +SKIP
-
-* SI units that were named after a person should be lower case except at
-  the beginning of a sentence, even if their symbol is capitalized. For
-  example, kelvin is a unit while Kelvin was a scientist.
-
 Angular Frequencies
 ===================
 
@@ -585,12 +729,12 @@ s) and angular frequency (rad / s).  An explicit way to do this
 conversion is to set up an equivalency between cycles/s and Hz:
 
 >>> from astropy import units as u
->>> f_ce = omega_ce.to(u.Hz, equivalencies=[(u.cy/u.s, u.Hz)])   # doctest: +SKIP
+>>> f_ce = omega_ce.to(u.Hz, equivalencies=[(u.cy/u.s, u.Hz)])  # doctest: +SKIP
 
 However, ``dimensionless_angles`` does work when dividing a velocity
 by an angular frequency to get a length scale:
 
->>> d_i = (c/omega_pi).to(u.m, equivalencies=u.dimensionless_angles())    # doctest: +SKIP
+>>> d_i = (c/omega_pi).to(u.m, equivalencies=u.dimensionless_angles())  # doctest: +SKIP
 
 .. _example_notebooks:
 
@@ -640,5 +784,9 @@ the README file of `benchmarks-repo`_.
 
 .. _ASCII: https://en.wikipedia.org/wiki/ASCII
 .. _cognitive complexity: https://www.sonarsource.com/docs/CognitiveComplexity.pdf
+.. _example notebook on particles: ../notebooks/getting_started/particles.ipynb
+.. _example notebook on units: ../notebooks/getting_started/units.ipynb
 .. _extract function refactoring pattern: https://refactoring.guru/extract-method
+.. _NumPy Enhancement Proposal 29: https://numpy.org/neps/nep-0029-deprecation_policy.html
+.. _pyupgrade: https://github.com/asottile/pyupgrade
 .. _rename refactoring in PyCharm: https://www.jetbrains.com/help/pycharm/rename-refactorings.html
diff --git a/docs/contributing/doc_guide.rst b/docs/contributing/doc_guide.rst
index 7e19a892..0287a4d2 100644
--- a/docs/contributing/doc_guide.rst
+++ b/docs/contributing/doc_guide.rst
@@ -562,7 +562,7 @@ Here is an example docstring in the numpydoc_ format:
 
       Returns
       -------
-      difference : float
+      float
           The difference between ``a`` and ``b``.
 
       Raises
@@ -946,12 +946,73 @@ Docstring guidelines
   derivations and extensive discussions of mathematics in the "Notes"
   section.
 
-* Docstrings may include a Raises_ section that describes which
-  exceptions get raised and under what conditions. This section should
-  be used only for errors that are non-obvious or have a large chance of
-  getting raised. Documenting exceptions makes it easier for future
-  developers to plan exception handling. However, in practice this
-  section often becomes outdated.
+* Describe each :term:`parameter` in the "Parameters_" section of the
+  docstring.
+
+  .. code-block:: rst
+
+     Parameters
+     ----------
+     x : float
+        Description of ``x``.
+
+     y : bool, optional, default: True
+        Description of ``y``.
+
+  * Place type information to the right of the parameter name, along
+    with ``optional`` and/or ``keyword-only`` if necessary.
+
+  * Describe any requirements for the parameter, including preconditions
+    specified using |validate_quantities| or |particle_input|.
+
+  * Use the substitution ``|array_like|`` to indicate that an argument
+    should be able to be converted into an |ndarray|.
+
+  * If the shapes and sizes of the parameters are interrelated, then
+    include that information in parentheses immediately before the type
+    information. Include a trailing comma inside the parentheses when
+    the parameter is 1D.
+
+    .. code-block:: rst
+
+       Parameters
+       ----------
+       a : (M,) array_like
+           First input vector.
+
+       b : (N,) array_like
+           Second input vector.
+
+       out : (M, N) ndarray, optional
+           A location where the result is stored.
+
+    Use an asterisk (``*``) for a dimension that can be of arbitrary
+    size, and an ellipsis (``...``) to represent an arbitrary number of
+    dimensions that can each be of arbitrary size.
+
+* Docstrings may include a "Raises_" section that describes which
+  exceptions get raised and under what conditions, and a "Warns_"
+  section that describes which warnings will be issued and for what
+  reasons.
+
+  * The "Raises_" and "Warns_" sections should only include exceptions
+    and warnings that are not obvious or have a high probability of
+    occurring. For example, the "Raises_" section should usually not
+    include a `TypeError` for when an :term:`argument` is not of the
+    type that is listed in the "Parameters_" section of the docstring.
+
+  * The "Raises_" section should include all exceptions that could
+    reasonably be expected to require exception handling.
+
+  * The "Raises_" section should be more complete for functionality that
+    is frequently used (e.g., |Particle|).
+
+  * The "Raises_" and "Warns_" sections should typically only include
+    exceptions and warnings that are raised or issued by the function
+    itself. Exceptions and warnings from commonly used decorators like
+    |validate_quantities| and |particle_input| should usually not be
+    included in these sections, but may be included if there is
+    strong justification to do so.
 
 * Private code objects (e.g., code objects that begin with a single
   underscore, like ``_private_object``) should have docstrings. A
@@ -1051,6 +1112,7 @@ Narrative documentation guidelines
 .. _doctests: https://docs.pytest.org/en/6.2.x/doctest.html
 .. _nested inline markup: https://docutils.sphinx-users.jp/docutils/docs/dev/rst/alternatives.html#nested-inline-markup
 .. _options to sphinx-build: https://www.sphinx-doc.org/en/master/man/sphinx-build.html#options
+.. _parameters: https://numpydoc.readthedocs.io/en/latest/format.html#parameters
 .. _raise an issue: https://github.com/PlasmaPy/PlasmaPy/issues/new?title=Improve+documentation+for...&labels=Documentation
 .. _raises: https://numpydoc.readthedocs.io/en/latest/format.html#raises
 .. _raw string: https://docs.python.org/3/reference/lexical_analysis.html#literals
@@ -1058,3 +1120,4 @@ Narrative documentation guidelines
 .. _Sphinx's glossary: https://www.sphinx-doc.org/en/master/glossary.html
 .. _Sphinx's templating page: https://www.sphinx-doc.org/en/master/templating.html
 .. _style overrides: https://docs.readthedocs.io/en/stable/guides/adding-custom-css.html
+.. _warns: https://numpydoc.readthedocs.io/en/latest/format.html#warns
diff --git a/docs/whatsnew/0.7.0.rst b/docs/whatsnew/0.7.0.rst
index 4baa27e3..1c113710 100644
--- a/docs/whatsnew/0.7.0.rst
+++ b/docs/whatsnew/0.7.0.rst
@@ -98,7 +98,7 @@ Deprecations and Removals
 Features
 --------
 
-- Add the `~plasmapy.particles.ionization_state.IonizationState.average_ion`
+- Added the `~plasmapy.particles.ionization_state.IonizationState.average_ion`
   method to |IonizationState|. (`#1028 <https://github.com/plasmapy/plasmapy/pull/1028>`__)
 - Added the
   `~plasmapy.particles.ionization_state_collection.IonizationStateCollection.average_ion`
diff --git a/plasmapy/particles/__init__.py b/plasmapy/particles/__init__.py
index f69a59b0..618759ba 100644
--- a/plasmapy/particles/__init__.py
+++ b/plasmapy/particles/__init__.py
@@ -13,7 +13,6 @@
     common_isotopes,
     electric_charge,
     half_life,
-    integer_charge,
     is_stable,
     isotopic_abundance,
     known_isotopes,
diff --git a/plasmapy/particles/atomic.py b/plasmapy/particles/atomic.py
index aa228865..01665b24 100644
--- a/plasmapy/particles/atomic.py
+++ b/plasmapy/particles/atomic.py
@@ -6,7 +6,6 @@
     "common_isotopes",
     "electric_charge",
     "half_life",
-    "integer_charge",
     "isotopic_abundance",
     "is_stable",
     "known_isotopes",
@@ -354,20 +353,6 @@ def charge_number(particle: Particle) -> Integral:
     return particle.charge_number
 
 
-@deprecated(
-    since="0.7.0",
-    warning_type=PlasmaPyFutureWarning,
-    message=(
-        "integer_charge has been deprecated and will be removed in a "
-        "future release.  Use charge_number instead."
-    ),
-)
-@particle_input(any_of={"charged", "uncharged"})
-def integer_charge(particle: Particle):
-    """Return the charge number of the particle."""
-    return charge_number(particle)
-
-
 @particle_input(any_of={"charged", "uncharged"})
 def electric_charge(particle: Particle) -> u.C:
     """
diff --git a/plasmapy/particles/ionization_state.py b/plasmapy/particles/ionization_state.py
index e0e1e544..2698977b 100644
--- a/plasmapy/particles/ionization_state.py
+++ b/plasmapy/particles/ionization_state.py
@@ -118,21 +118,6 @@ def charge_number(self) -> Integral:
         """The charge number of the ion."""
         return self.ion.charge_number
 
-    @property
-    @deprecated(
-        since="0.7.0",
-        name="integer_charge",
-        message=(
-            "The integer_charge attribute of IonicLevel has been "
-            "deprecated and will be removed in a future release.  Use "
-            "charge_number instead."
-        ),
-        warning_type=PlasmaPyFutureWarning,
-    )
-    def integer_charge(self) -> Integral:
-        """The charge number of the ion."""
-        return self.charge_number
-
     @property
     def ionic_fraction(self) -> Real:
         r"""
@@ -730,21 +715,6 @@ def charge_numbers(self) -> np.ndarray:
         """An array of the charge numbers."""
         return self.to_list().charge_number
 
-    @property
-    @deprecated(
-        since="0.7.0",
-        name="integer_charges",
-        message=(
-            "The integer_charges attribute of IonizationState has been "
-            "deprecated and will be removed in a future release.  Use "
-            "charge_numbers instead."
-        ),
-        warning_type=PlasmaPyFutureWarning,
-    )
-    def integer_charges(self) -> np.ndarray:
-        """An array of the charge numbers."""
-        return self.charge_numbers
-
     @property
     def Z_mean(self) -> np.float64:
         """Return the mean charge number."""
diff --git a/plasmapy/particles/particle_class.py b/plasmapy/particles/particle_class.py
index dec947e4..5e4630e7 100644
--- a/plasmapy/particles/particle_class.py
+++ b/plasmapy/particles/particle_class.py
@@ -957,21 +957,6 @@ def charge_number(self) -> Integral:
             raise ChargeError(f"The charge of particle {self} has not been specified.")
         return self._attributes["charge number"]
 
-    @property
-    @deprecated(
-        since="0.7.0",
-        name="integer_charge",
-        message=(
-            "The integer_charge attribute of Particle has been "
-            "deprecated and will be removed in a future release.  Use "
-            "charge_number instead."
-        ),
-        warning_type=PlasmaPyFutureWarning,
-    )
-    def integer_charge(self) -> Integral:
-        """The particle's electrical charge in units of the elementary charge."""
-        return self.charge_number
-
     @property
     def charge(self) -> u.C:
         """
diff --git a/setup.cfg b/setup.cfg
index f1420531..9772e69c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -83,6 +83,7 @@ tests =
     pytest >= 5.4.0
     pytest-regressions
     pytest-xdist
+    tomli
 docs =
     # ought to mirror requirements/docs.txt
     %(extras)s
diff --git a/tox.ini b/tox.ini
index f801ede0..9a647925 100644
--- a/tox.ini
+++ b/tox.ini
@@ -91,7 +91,7 @@ conda_deps =
 # This env tests minimal versions of each dependency.
 [testenv:py38-all-minimal]
 basepython = python3.8
-extras =
+extras = tests
 deps =
     astropy == 4.3.1
     cached-property == 1.5.2


Test patch:
diff --git a/plasmapy/particles/tests/test_atomic.py b/plasmapy/particles/tests/test_atomic.py
index 2b69dcc4..8837e7b1 100644
--- a/plasmapy/particles/tests/test_atomic.py
+++ b/plasmapy/particles/tests/test_atomic.py
@@ -12,7 +12,6 @@
     common_isotopes,
     electric_charge,
     half_life,
-    integer_charge,
     is_stable,
     isotopic_abundance,
     known_isotopes,
@@ -34,7 +33,6 @@
     ParticleWarning,
 )
 from plasmapy.particles.symbols import atomic_symbol, element_name, isotope_symbol
-from plasmapy.utils.exceptions import PlasmaPyFutureWarning
 from plasmapy.utils.pytest_helpers import run_test
 
 # function to be tested, argument(s), expected result/outcome
@@ -537,8 +535,3 @@ def test_missing_atomic_data(self):
 @pytest.mark.parametrize("particle, electron", str_electron_table)
 def test_is_electron(particle, electron):
     assert _is_electron(particle) == electron
-
-
-def test_integer_charge():
-    with pytest.warns(PlasmaPyFutureWarning):
-        assert integer_charge("Fe 20+") == 20
diff --git a/plasmapy/particles/tests/test_exceptions.py b/plasmapy/particles/tests/test_exceptions.py
index 2cb0d90c..66774755 100644
--- a/plasmapy/particles/tests/test_exceptions.py
+++ b/plasmapy/particles/tests/test_exceptions.py
@@ -7,10 +7,10 @@
 from plasmapy.particles import IonizationState, IonizationStateCollection
 from plasmapy.particles.atomic import (
     atomic_number,
+    charge_number,
     common_isotopes,
     electric_charge,
     half_life,
-    integer_charge,
     is_stable,
     isotopic_abundance,
     known_isotopes,
@@ -30,7 +30,6 @@
 )
 from plasmapy.particles.nuclear import nuclear_binding_energy, nuclear_reaction_energy
 from plasmapy.particles.symbols import atomic_symbol, element_name, isotope_symbol
-from plasmapy.utils.exceptions import PlasmaPyFutureWarning
 
 tests_for_exceptions = {
     "too few nstates": (
@@ -826,7 +825,7 @@ def test_named_tests_for_exceptions(tested_object, args, kwargs, expected_except
         pytest.warns(ParticleWarning),
     ],
     [
-        integer_charge,
+        charge_number,
         [
             "fads",
         ],
@@ -834,7 +833,7 @@ def test_named_tests_for_exceptions(tested_object, args, kwargs, expected_except
         pytest.raises(InvalidParticleError),
     ],
     [
-        integer_charge,
+        charge_number,
         [
             "H++",
         ],
@@ -842,7 +841,7 @@ def test_named_tests_for_exceptions(tested_object, args, kwargs, expected_except
         pytest.raises(InvalidParticleError),
     ],
     [
-        integer_charge,
+        charge_number,
         [
             "h+",
         ],
@@ -850,7 +849,7 @@ def test_named_tests_for_exceptions(tested_object, args, kwargs, expected_except
         pytest.raises(InvalidParticleError),
     ],
     [
-        integer_charge,
+        charge_number,
         [
             "fe 1+",
         ],
@@ -858,7 +857,7 @@ def test_named_tests_for_exceptions(tested_object, args, kwargs, expected_except
         pytest.raises(InvalidParticleError),
     ],
     [
-        integer_charge,
+        charge_number,
         [
             "d+",
         ],
@@ -866,7 +865,7 @@ def test_named_tests_for_exceptions(tested_object, args, kwargs, expected_except
         pytest.raises(InvalidParticleError),
     ],
     [
-        integer_charge,
+        charge_number,
         [
             "Fe 29+",
         ],
@@ -874,37 +873,13 @@ def test_named_tests_for_exceptions(tested_object, args, kwargs, expected_except
         pytest.raises(InvalidParticleError),
     ],
     [
-        integer_charge,
+        charge_number,
         [
             "H-1",
         ],
         {},
         pytest.raises(ChargeError),
     ],
-    [
-        integer_charge,
-        [
-            "H---",
-        ],
-        {},
-        pytest.warns(PlasmaPyFutureWarning),
-    ],
-    [
-        integer_charge,
-        [
-            "Fe -26",
-        ],
-        {},
-        pytest.warns(PlasmaPyFutureWarning),
-    ],
-    [
-        integer_charge,
-        [
-            "Og 10-",
-        ],
-        {},
-        pytest.warns(PlasmaPyFutureWarning),
-    ],
     [
         isotope_symbol,
         ("Md-260",),
@@ -1010,7 +985,6 @@ def test_named_tests_for_exceptions(tested_object, args, kwargs, expected_except
     stable_isotopes,
     common_isotopes,
     isotopic_abundance,
-    integer_charge,
     electric_charge,
 ]
 
diff --git a/plasmapy/particles/tests/test_ionization_state.py b/plasmapy/particles/tests/test_ionization_state.py
index ed6dda04..40795ff3 100644
--- a/plasmapy/particles/tests/test_ionization_state.py
+++ b/plasmapy/particles/tests/test_ionization_state.py
@@ -45,11 +45,6 @@ def test_ionic_level_attributes(ion, ionic_fraction, number_density):
     assert u.isclose(instance.number_density, number_density, equal_nan=True)
     assert instance.charge_number == charge_number(ion)
 
-    # TODO: remove when IonicLevel.integer_charge is removed
-    with pytest.warns(PlasmaPyFutureWarning):
-        integer_charge = instance.integer_charge
-    assert integer_charge == charge_number(ion)
-
 
 @pytest.mark.parametrize(
     "invalid_fraction, expected_exception",
@@ -166,19 +161,6 @@ def test_charge_numbers(self, test_name):
         expected_charge_numbers = np.arange(instance.atomic_number + 1)
         assert np.allclose(instance.charge_numbers, expected_charge_numbers)
 
-    @pytest.mark.parametrize("test_name", test_names)
-    def test_integer_charges(self, test_name):
-        """
-        Test that `IonizationState.integer_charges` has the
-        correct charge numbers and issues a future warning.
-        """
-        # TODO: remove when IonizationState.integer_charge is removed
-        instance = self.instances[test_name]
-        expected_charge_numbers = np.arange(instance.atomic_number + 1)
-        with pytest.warns(PlasmaPyFutureWarning):
-            actual_integer_charges = instance.integer_charges
-        assert np.allclose(actual_integer_charges, expected_charge_numbers)
-
     @pytest.mark.parametrize(
         "test_name",
         [name for name in test_names if "ionic_fractions" in test_cases[name]],
@@ -474,7 +456,7 @@ def test_IonizationState_ionfracs_from_ion_input(ion):
         actual_ionic_fractions,
         atol=1e-16,
         err_msg=f"The returned ionic fraction for IonizationState({repr(ion)}) "
-        f"should have entirely been in the Z = {ion_particle.integer_charge} "
+        f"should have entirely been in the Z = {ion_particle.charge_number} "
         f"level.",
     )
 
diff --git a/plasmapy/particles/tests/test_particle_class.py b/plasmapy/particles/tests/test_particle_class.py
index a52c2da9..f1f52d90 100644
--- a/plasmapy/particles/tests/test_particle_class.py
+++ b/plasmapy/particles/tests/test_particle_class.py
@@ -1421,11 +1421,6 @@ def test_particle_is_category_valid_categories():
     assert some_valid_categories.issubset(Particle.is_category.valid_categories)
 
 
-def test_deprecated_integer_charge():
-    with pytest.warns(PlasmaPyFutureWarning):
-        assert Particle("e-").integer_charge == -1
-
-
 def test_CustomParticle_cmp():
     """Test ``__eq__`` and ``__ne__`` in the CustomParticle class."""
     particle1 = CustomParticle(2 * 126.90447 * u.u, 0 * u.C, "I2")
diff --git a/plasmapy/tests/test_requirements.py b/plasmapy/tests/test_requirements.py
new file mode 100644
index 00000000..8f1f78cb
--- /dev/null
+++ b/plasmapy/tests/test_requirements.py
@@ -0,0 +1,141 @@
+"""Tests for the consistency of requirements."""
+
+import os
+import pathlib
+import pytest
+import setuptools
+import tomli
+
+from typing import Dict, Set
+
+import plasmapy
+
+base_directory = pathlib.Path(__file__).parents[2]
+requirements_directory = base_directory / "requirements"
+requirements_prefixes = ("build", "docs", "extras", "install", "tests")
+
+
+def read_requirements_txt_file(prefix: str, requirements_directory: str) -> Set[str]:
+    """
+    Read in a text file containing requirements.
+
+    Parameters
+    ----------
+    prefix : str
+        The prefix to a filename, such as ``"build"`` for ``build.txt``.
+
+    requirements_directory : str
+        The path to the directory containing the requirements file.
+
+    Returns
+    -------
+    list of str
+        A `list` containing the lines of the requirements file,
+        excluding lines that do not start with an alphabetic character
+        (like comments).
+    """
+    file = (requirements_directory / prefix).with_suffix(".txt")
+    lines_of_file = file.read_text().splitlines()
+    return {line.strip() for line in lines_of_file if line[0].isalpha()}
+
+
+def get_requirements_from_txt() -> Dict[str, Set[str]]:
+    """
+    Get the requirements from the ``.txt`` files in the ``requirements``
+    directory.
+    """
+    return {
+        prefix: read_requirements_txt_file(prefix, requirements_directory)
+        for prefix in requirements_prefixes
+    }
+
+
+def get_requirements_from_setup_cfg() -> Dict[str, Set[str]]:
+    """Get the requirements that are contained in ``setup.cfg``."""
+    configuration = setuptools.config.read_configuration(f"{base_directory}/setup.cfg")
+    d = {
+        key: set(configuration["options"]["extras_require"][key])
+        for key in "docs extras tests".split()
+    }
+    d["install"] = set(configuration["options"]["install_requires"])
+    return d
+
+
+def get_requirements_from_pyproject_toml() -> Dict[str, Set[str]]:
+    """Get the requirements that are contained in pyproject.toml."""
+    with open(f"{base_directory}/pyproject.toml", "rb") as pyproject:
+        pyproject_toml = tomli.load(pyproject)
+    return {"build": set(pyproject_toml["build-system"]["requires"])}
+
+
+requirements_from_txt = get_requirements_from_txt()
+requirements_from_pyproject_toml = get_requirements_from_pyproject_toml()
+requirements_from_setup_cfg = get_requirements_from_setup_cfg()
+
+requirements_table = [
+    (
+        "build",
+        requirements_from_txt["build"],
+        "requirements/build.txt",
+        requirements_from_pyproject_toml["build"],
+        "pyproject.toml",
+    ),
+    (
+        "install",
+        requirements_from_txt["install"],
+        "requirements/install.txt",
+        requirements_from_setup_cfg["install"],
+        "setup.cfg",
+    ),
+    (
+        "extras",
+        requirements_from_txt["extras"],
+        "requirements/extras.txt",
+        requirements_from_setup_cfg["extras"],
+        "setup.cfg",
+    ),
+    (
+        "docs",
+        requirements_from_txt["docs"],
+        "requirements/docs.txt",
+        requirements_from_setup_cfg["docs"] - requirements_from_txt["extras"],
+        "setup.cfg",
+    ),
+    (
+        "tests",
+        requirements_from_txt["tests"],
+        "requirements/tests.txt",
+        requirements_from_setup_cfg["tests"] - requirements_from_txt["extras"],
+        "setup.cfg",
+    ),
+]
+
+
+@pytest.mark.parametrize("prefix, req1, file1, req2, file2", requirements_table)
+def test_consistency_of_requirements(
+    prefix: str, req1: Set[str], file1: str, req2: Set[str], file2: str
+):
+    """
+    Check that the ``prefix``-category of requirements in ``req1`` (from
+    ``file1``) match the requirements in ``req2`` (from ``file2``).
+
+    Note that in ``setup.cfg``, the ``docs`` and ``tests`` requirements
+    also include ``extras``, so these should be subtracted out in the
+    parametrization for this test.
+    """
+    error_messages = []
+
+    if requirements_in_req1_but_not_req2 := req1 - req2:
+        error_messages.append(
+            f"The following {prefix} requirements are in {file1} but "
+            f"not {file2}: {requirements_in_req1_but_not_req2}."
+        )
+
+    if requirements_in_rec2_but_not_rec1 := req2 - req1:
+        error_messages.append(
+            f"The following {prefix} requirements are in {file2} but "
+            f"not {file1}: {requirements_in_rec2_but_not_rec1}."
+        )
+
+    if errmsg := "".join(error_messages).strip():
+        pytest.fail(errmsg)
diff --git a/requirements/tests.txt b/requirements/tests.txt
index ca8f9705..867454ec 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -17,3 +17,4 @@ pydocstyle
 pytest >= 5.4.0
 pytest-regressions
 pytest-xdist
+tomli