-
Notifications
You must be signed in to change notification settings - Fork 340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Test helper refactoring 2: Refactor and move functionality for generating call strings used in error messages #920
Conversation
This originally came from PlasmaPy#726, but was spun off into a different PR.
Codecov Report
@@ Coverage Diff @@
## master #920 +/- ##
==========================================
+ Coverage 96.29% 96.31% +0.02%
==========================================
Files 62 61 -1
Lines 5715 5341 -374
==========================================
- Hits 5503 5144 -359
+ Misses 212 197 -15
Continue to review full report at Codecov.
|
9ecd89b
to
a40700c
Compare
|
I set this warning to not be included in coverage tests because it's likely that we'll have times when no functions have been deprecated.
(5.3 * u.m, "5.3*u.m"), | ||
(5.4 / u.m, "5.4/u.m"), | ||
(5.5 * u.m ** -2, "5.5*u.m**-2"), | ||
(5.0 * u.dimensionless_unscaled, "5.0*u.dimensionless_unscaled"), | ||
(np.array([3.5, 4.2]) * u.m ** -2.5, "np.array([3.5, 4.2])*u.m**-2.5"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For completeness, maybe throw in a case that looks like 5.0 / u.m ** 3
.
function_for_type = { | ||
u.Quantity: _code_repr_of_quantity, | ||
np.ndarray: _code_repr_of_ndarray, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably add _code_repr_...
's for long lists, tuples, and dicts. I'm guessing these cases will turn up quickly.
(1, "1"), | ||
("asdf", "'asdf'"), | ||
(3.42, "3.42"), | ||
([3.42, 3.84], "[3.42, 3.84]"), | ||
(4.2 * u.m, "4.2*u.m"), | ||
(np.array([1, 2, 3]), "np.array([1, 2, 3])"), | ||
(np.array([[1, 2], [3, 4]]), "np.array([[1, 2], [3, 4]])"), | ||
(np.array([1.0, 2.0, 3.0]) * u.m / u.s, "np.array([1., 2., 3.])*u.m/u.s"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about test cases for lists, tuples, and dicts?
args_and_kwargs = _code_repr_of_args_and_kwargs(args, kwargs) | ||
if args_and_kwargs != expected: | ||
pytest.fail( | ||
f"_code_repr_of_args_and_kwargs with the following arguments:\n" | ||
f" args: {args}\n" | ||
f" kwargs: {kwargs}\n" | ||
f"is not returning the expected string:\n" | ||
f" expected: {repr(expected)}\n" | ||
f" actual: {repr(args_and_kwargs)}" | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing and reporting failures this way seems like it's sidestepping the assert
mechanics. Why not do something more like...
args_and_kwargs = _code_repr_of_args_and_kwargs(args, kwargs) | |
if args_and_kwargs != expected: | |
pytest.fail( | |
f"_code_repr_of_args_and_kwargs with the following arguments:\n" | |
f" args: {args}\n" | |
f" kwargs: {kwargs}\n" | |
f"is not returning the expected string:\n" | |
f" expected: {repr(expected)}\n" | |
f" actual: {repr(args_and_kwargs)}" | |
) | |
args_and_kwargs = _code_repr_of_args_and_kwargs(args, kwargs) | |
assert args_and_kwargs == expected, ( | |
f"_code_repr_of_args_and_kwargs with the following arguments:\n" | |
f" args: {args}\n" | |
f" kwargs: {kwargs}\n" | |
f"is not returning the expected string:\n" | |
f" expected: {repr(expected)}\n" | |
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think this approach should be applied to all similar cases.
# function, args, kwargs, expected | ||
call_string_table = [ | ||
(generic_function, (), {}, "generic_function()"), | ||
(generic_function, (1), {}, "generic_function(1)"), | ||
(generic_function, ("x"), {}, "generic_function('x')"), | ||
(generic_function, (1, "b", {}), {}, "generic_function(1, 'b', {})"), | ||
(generic_function, (), {"kw": 1}, "generic_function(kw=1)"), | ||
(generic_function, (), {"x": "c"}, "generic_function(x='c')"), | ||
( | ||
generic_function, | ||
(1, 2, 3), | ||
{"a": 4, "b": 5, "c": 6}, | ||
"generic_function(1, 2, 3, a=4, b=5, c=6)", | ||
), | ||
( | ||
generic_function, | ||
np.array((1.0, 2.0)), | ||
{}, | ||
"generic_function(np.array([1., 2.]))", | ||
), | ||
( | ||
generic_function, | ||
np.array((3.0, 4.0)) * u.m, | ||
{}, | ||
"generic_function(np.array([3., 4.])*u.m)", | ||
), | ||
( | ||
generic_function, | ||
(ValueError,), | ||
{"x": TypeError}, | ||
"generic_function(ValueError, x=TypeError)", | ||
), | ||
(generic_function, (), {"bowl": super}, "generic_function(bowl=super)"), | ||
( | ||
generic_function, | ||
(1, "b"), | ||
{"b": 42, "R2": "D2"}, | ||
"generic_function(1, 'b', b=42, R2='D2')", | ||
), | ||
] | ||
|
||
|
||
@pytest.mark.parametrize("function,args,kwargs,expected", call_string_table) | ||
def test_call_string(function, args, kwargs, expected): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless call_string_table
(and the like below) is (are) going to be called in several locations, I find that this disassociation of the parametrized list and the test function to significantly hinder readability of tests. I mean, I start reading the test and then I have to wonder away from the test to get the cases that are being tested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the main problem with readability here is that the when we parametrize tests using a list containing a series of tuples which might contain inner tuples, the parameters get detached from the corresponding semantic information/metadata. It'd be so much more readable if the arguments were defined like keywords. In #925 I define classes that are intended to address this problem...without me having to rewrite essentially the same code over and over again. I might try doing something like that here, though independent of #925, since I'm trying to avoid using the test helpers to test the test helpers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with that sentiment. pytest.mark.parametrize
's separation of names and values does make it a bit difficult to follow.
My point here is that when someone reads @pytest.mark.parametrize("function,args,kwargs,expected", call_string_table)
they have to immediately search the code to find call_string_table
. The fact that I have to leave the test to find out what is being tested is what reduces readability for me.
Co-authored-by: Erik Everson <[email protected]>
Co-authored-by: Erik Everson <[email protected]>
Co-authored-by: Erik Everson <[email protected]>
Co-authored-by: Erik Everson <[email protected]>
The docstring now says that these functions **approximate** the call string, which is easier to promise than providing an exact docstring.
- Use assert instead of pytest.fail - Define namedtuples to provide semantic information in parametrize
The name cls_args and cls_kwargs are potentially ambiguous, since it could mean that these are args associated with a class or a class instance. These are renamed to args_to_cls and kwargs_to_cls (and similarly for method_args and method_kwargs).
Changes:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm satisfied. 😁 ... Looks good. The namedtuple
is an interesting approach to the problem, would not have thought about it myself. I'm guessing you decided to leave the code_repr
functions for lists, tuples, and dicts for a separate PR, correct?
quantity_case(quantity=5.5 * u.m ** -2, expected="5.5*u.m**-2"), | ||
quantity_case( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
quantity_case(quantity=5.5 * u.m ** -2, expected="5.5*u.m**-2"), | |
quantity_case( | |
quantity_case(quantity=5.5 * u.m ** -2, expected="5.5*u.m**-2"), | |
quantity_case(quantity=5.5 / u.m ** 2, expected="5.5*u.m**-2"), | |
quantity_case( |
The second spin-off PR from #726. This should be reviewed and merged after #919.
We currently have a function
call_string
which generates a string that can reproduce a particular call. This function is currently used in test helper functionality when generating error messages. Its potential uses go beyond creating error messages, though, such as in__repr__
methods. This PR moves that functionality toplasmapy.utils.formatting
, and creates functions for reproducing the call strings of functions and attributes/methods of class instances.