Repository: PlasmaPy/PlasmaPy
Pull request #999 (2021-02-04 20:30:13)

Add can_be_zero option to CheckValues
<!--
Thank you for contributing to PlasmaPy! Here's a bunch of pointers to
make things easier for all of us:

* If this PR will solve an issue tracked by GitHub, then please add
  "Closes #42" so the issue automatically closes once this pull request
  is merged.  In this example, issue #42 would have been closed.
  If your PR will not completely solve the issue, then please still reference
  still reference the issue like "issue #42".  (For more info see [GitHub's primer
  on linking issues](https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword).)

* Remember to add some description of your changes in this text box.

* If your pull request is not yet ready for review - either because
  you're just looking for feedback on a change or because you're not
  perfectly satisfied with your change - submit it as a draft pull request.
  Remember to change its' status once it's ready.

* If this is your first contribution, please add your name to the author
  list in `docs/about/credits.rst`.

* Feel free to chat with other developers on our Matrix channel at:
   https://app.element.io/#/room/#plasmapy:openastronomy.org

* We have a developer's guide to help answer some of your questions.
  http://docs.plasmapy.org/en/latest/development/index.html

Many thanks in advance for following these pointers and for being willing to contribute!

When submitting a pull request, please ensure that you can (eventually,
sometime before it is merged) check the following basic requirements:

-->

- [x] I have added a changelog entry for this pull request.

<!--

In short: A changelog entry is a short description of your PR's changes.
Each entry is written in a `<PULL REQUEST>.<TYPE>.rst` file and stored in
the `changelog` directory,  where `<PULL REQUEST>` is a pull request
number and `<TYPE>` is one of:

* `breaking`: A change which requires users to change code and is not backwards compatible. (Not to be used for removal of deprecated features.)
* `feature`: New user facing features and any new behavior.
* `bugfix`: Fixes a reported bug.
* `doc`: Documentation addition or improvement, like rewording an entire session or adding missing docs.
* `removal`: Feature deprecation and/or feature removal.
* `trivial`: A change which has no user facing effect or is tiny change.

A PR number is generated after you successfully submit a new PR, so the changelog
file can only be added after you open the PR.```

For more information, see:
https://github.com/PlasmaPy/PlasmaPy/blob/master/changelog/README.rst

--->

- [x] If adding new functionality, I have added tests and
      docstrings.

<!--
(Tests pop up at the bottom, in the checks box.)
-->

- [x] I have fixed any newly failing tests.

<!--
(If you're unsure why they're failing, ask!)
-->


davemus: Looks like I've wrecked this PR up via `git push --force`.

```
Error: reserveCache failed: Cache already exists. Scope: refs/pull/999/merge, Key: pre-commit-2-8ab1628a0b82eeb07768adee6d698412385f24927008fde5efe51739490a6721-0e81c8932fc58ad077bfad29c89a1c156ebad8c9af0cffc7c6c1e8f84086555d, Version: 28cdb9f5496f334116f23e86f0063f5d3a9348c2e22425a33171e071aadada7e
```
Should I close this and reopen?
rocco8773: > 
> 
> Looks like I've wrecked this PR up via `git push --force`.
> 
> ```
> Error: reserveCache failed: Cache already exists. Scope: refs/pull/999/merge, Key: pre-commit-2-8ab1628a0b82eeb07768adee6d698412385f24927008fde5efe51739490a6721-0e81c8932fc58ad077bfad29c89a1c156ebad8c9af0cffc7c6c1e8f84086555d, Version: 28cdb9f5496f334116f23e86f0063f5d3a9348c2e22425a33171e071aadada7e
> ```
> 
> Should I close this and reopen?

@davemus Just re-running the checks seemed to fix the problem.  Let's hope it sticks.  I don't think we have to worry about a new PR at this point.

----

@davemus Are you ready for a review on this?  I'm quite busy today, but I'll trying to give it a once over today or tomorrow.
davemus: > @davemus Are you ready for a review on this? I'm quite busy today, but I'll trying to give it a once over today or tomorrow.

Yes I'm ready. That's okay. As for reviews I can't answer asap: my availability time is a period from 6 till 8 and from 18 till 21 (UTC).

Patch:
diff --git a/changelog/999.feature.rst b/changelog/999.feature.rst
new file mode 100644
index 00000000..18c2e7bb
--- /dev/null
+++ b/changelog/999.feature.rst
@@ -0,0 +1,5 @@
+Added new ``can_be_zero`` check parameter to
+`~plasmapy.utils.decorators.checks.CheckValues` and its dependents (
+`~plasmapy.utils.decorators.checks.check_values`,
+`~plasmapy.utils.decorators.validators.ValidateQuantities`,
+`~plasmapy.utils.decorators.validators.validate_quantities`).
diff --git a/plasmapy/utils/decorators/checks.py b/plasmapy/utils/decorators/checks.py
index c8d62d8a..3513a608 100644
--- a/plasmapy/utils/decorators/checks.py
+++ b/plasmapy/utils/decorators/checks.py
@@ -95,6 +95,7 @@ class CheckValues(CheckBase):
         can_be_inf       `bool`  [DEFAULT `True`] values can be :data:`~numpy.inf`
         can_be_nan       `bool`  [DEFAULT `True`] values can be :data:`~numpy.nan`
         none_shall_pass  `bool`  [DEFAULT `False`] values can be a python `None`
+        can_be_zero      `bool`  [DEFAULT `True`] values can be zero
         ================ ======= ================================================
 
     Notes
@@ -133,6 +134,7 @@ def bar(self, arg1, arg2):
         "can_be_inf": True,
         "can_be_nan": True,
         "none_shall_pass": False,
+        "can_be_zero": True,
     }
 
     def __init__(
@@ -313,12 +315,8 @@ def _check_value(self, arg, arg_name: str, arg_checks: Dict[str, bool]):
                     raise ValueError(f"{valueerror_msg} Nones.")
 
             elif ckey == "can_be_negative":
-                if not arg_checks[ckey]:
-                    # Allow NaNs through without raising a warning
-                    with np.errstate(invalid="ignore"):
-                        isneg = np.any(arg < 0)
-                    if isneg:
-                        raise ValueError(f"{valueerror_msg} negative numbers.")
+                if not arg_checks[ckey] and np.any(arg < 0):
+                    raise ValueError(f"{valueerror_msg} negative numbers.")
 
             elif ckey == "can_be_complex":
                 if not arg_checks[ckey] and np.any(np.iscomplexobj(arg)):
@@ -332,6 +330,10 @@ def _check_value(self, arg, arg_name: str, arg_checks: Dict[str, bool]):
                 if not arg_checks["can_be_nan"] and np.any(np.isnan(arg)):
                     raise ValueError(f"{valueerror_msg} NaNs.")
 
+            elif ckey == "can_be_zero":
+                if not arg_checks[ckey] and np.any(arg == 0):
+                    raise ValueError(f"{valueerror_msg} zeros.")
+
 
 class CheckUnits(CheckBase):
     """
@@ -1212,6 +1214,7 @@ def check_values(
         can_be_inf       `bool`  [DEFAULT `True`] values can be :data:`~numpy.inf`
         can_be_nan       `bool`  [DEFAULT `True`] values can be :data:`~numpy.nan`
         none_shall_pass  `bool`  [DEFAULT `False`] values can be a python `None`
+        can_be_zero      `bool`  [DEFAULT `True`] values can be zero
         ================ ======= ================================================
 
     Notes
diff --git a/plasmapy/utils/decorators/validators.py b/plasmapy/utils/decorators/validators.py
index 9c763c57..cb13b2a8 100644
--- a/plasmapy/utils/decorators/validators.py
+++ b/plasmapy/utils/decorators/validators.py
@@ -57,6 +57,7 @@ class ValidateQuantities(CheckUnits, CheckValues):
         can_be_inf             `bool`  [DEFAULT `True`] values can be :data:`~numpy.inf`
         can_be_nan             `bool`  [DEFAULT `True`] values can be :data:`~numpy.nan`
         none_shall_pass        `bool`  [DEFAULT `False`] values can be a python `None`
+        can_be_zero            `bool`  [DEFAULT `True`] values can be zero
         ====================== ======= ================================================
 
     Notes
@@ -429,6 +430,7 @@ def validate_quantities(func=None, validations_on_return=None, **validations):
         can_be_inf             `bool`  [DEFAULT `True`] values can be :data:`~numpy.inf`
         can_be_nan             `bool`  [DEFAULT `True`] values can be :data:`~numpy.nan`
         none_shall_pass        `bool`  [DEFAULT `False`] values can be a python `None`
+        can_be_zero            `bool`  [DEFAULT `True`] values can be zero
         ====================== ======= ================================================
 
     Notes


Test patch:
diff --git a/plasmapy/utils/decorators/tests/test_checks.py b/plasmapy/utils/decorators/tests/test_checks.py
index b5728a00..2a443960 100644
--- a/plasmapy/utils/decorators/tests/test_checks.py
+++ b/plasmapy/utils/decorators/tests/test_checks.py
@@ -744,6 +744,7 @@ def test_cv_default_check_values(self):
             ("can_be_inf", True),
             ("can_be_nan", True),
             ("none_shall_pass", False),
+            ("can_be_zero", True),
         ]
         for key, val in _defaults:
             assert cv._CheckValues__check_defaults[key] == val
@@ -1003,6 +1004,22 @@ def test_cv_method__check_value(self):
                     "checks": {**default_checks, "none_shall_pass": True},
                 }
             },
+            # tests for check 'can_be_zero'
+            {
+                "input": {
+                    "args": [0, 0 * u.cm, np.arange(0, 3), np.arange(0, 3) * u.kg],
+                    "arg_name": "arg",
+                    "checks": {**default_checks, "can_be_zero": False},
+                },
+                "raises": ValueError,
+            },
+            {
+                "input": {
+                    "args": [0, 0 * u.cm, np.arange(0, 3), np.arange(0, 3) * u.kg],
+                    "arg_name": "arg",
+                    "checks": {**default_checks, "can_be_zero": True},
+                }
+            },
         ]
 
         # test
diff --git a/plasmapy/utils/decorators/tests/test_validators.py b/plasmapy/utils/decorators/tests/test_validators.py
index 025a1c58..1218be53 100644
--- a/plasmapy/utils/decorators/tests/test_validators.py
+++ b/plasmapy/utils/decorators/tests/test_validators.py
@@ -145,6 +145,25 @@ def test_vq_method__get_validations(self):
                 },
                 "raises": ValueError,
             },
+            {
+                "descr": "define both validations on args and validations_on_return",
+                "setup": {
+                    "function": self.foo,
+                    "args": (5,),
+                    "kwargs": {},
+                    "validations": {
+                        "x": {"units": [u.cm], "none_shall_pass": False},
+                        "validations_on_return": {
+                            "units": [u.cm],
+                            "can_be_zero": False,
+                        },
+                    },
+                },
+                "output": {
+                    "x": {"units": [u.cm], "none_shall_pass": False},
+                    "validations_on_return": {"units": [u.cm], "can_be_zero": False},
+                },
+            },
         ]  # type: List[Dict[str, Any]]
 
         for case in _cases: