Skip to content

WIP: ENH/TST: xp_assert_ enhancements #267

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

mdhaber
Copy link
Contributor

@mdhaber mdhaber commented Apr 16, 2025

Toward #17. As discussed in mdhaber/marray#110 (comment).

This adds a few of the features from SciPy's xp_assert_ functions to the xpx xp_assert functions. Tests will probably fail in this first commit; I'll comment inline.

I decided not to add a check_namespace toggle: if check_namespace is False and the namespaces don't match, other operations might fail. I didn't want to work on fixing this if it's not an important feature. There is only one file in SciPy that uses check_namespace=False, and it probably shouldn't.

Once this is looking good, I'll add xp_assert_less as a clone of xp_assert_equal.

Copy link
Contributor Author

@mdhaber mdhaber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easier to review with "Hide whitespace". @lucascolley do you see what is going on with

FAILED tests/test_testing.py::test_assert_close_equal_shape[sparse-True-xp_assert_close]
FAILED tests/test_testing.py::test_assert_close_equal_dtype[sparse-True-xp_assert_close]

?

Also, it looks to me like the codecov failures are misleading. It doesn't look to me like the new code is actually missing coverage if tests are run with all backends.

@@ -9,13 +9,15 @@
from types import ModuleType
from typing import cast

import numpy as np
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this OK? Sometimes it was imported within test functions below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today it is OK, as this module is not imported automatically from the outer scope.
In the long run though, we want to move this module to public at which point it won't be a good design anymore (although it remains to be seen if any Array library in real life can achieve not to have numpy as a hard dependency...)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it would be fine to make these public API once they are ready, with the caveat that NumPy is required. We are really striving for minimal runtime dependencies rather than test time dependencies downstream, at least for now.

Copy link
Contributor

@crusaderky crusaderky Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This module heavily relies on np.testing.assert* anyway.
We'll just need to add a test that import array_api_extra doesn't import numpy.

msg = f"dtypes do not match: {actual.dtype} != {desired.dtype}"
assert actual.dtype == desired.dtype, msg

if is_numpy_namespace(actual_xp) and check_scalar:
Copy link
Contributor Author

@mdhaber mdhaber Apr 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if is_numpy_namespace(actual_xp) and check_scalar:
if is_numpy_namespace(actual_xp) and check_arrayness:

?
I seem to remember some discussion about naming this parameter when adding to SciPy (check_0d). I don't really care what it is called. check_type is also on the table.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scalar sounds fine to me.

@mdhaber mdhaber marked this pull request as draft April 16, 2025 18:17
@lucascolley
Copy link
Member

lucascolley commented Apr 16, 2025

It doesn't look to me like the new code is actually missing coverage if tests are run with all backends.

Yeah, this is a constant problem. It eventually flips to passed once it registers the backends run. I think initial investigation didn't find a solution, but there is probably some sort of config/workaround to fix it.

@lucascolley
Copy link
Member

The failures are XPASSes: [XPASS(strict)] sparse:no isdtype

Judging by the message, we're expecting the test to fail due to sparse not implementing isdtype, but it is now passing. Could that be because of any of your changes?

@mdhaber
Copy link
Contributor Author

mdhaber commented Apr 16, 2025

That's what I figured, which is why I tried to set the xfail strict=False in param_assert_equal_close. I guess that option is not respected as written.

The trouble is that when check_shape or check_dtype is False, _check_ns_shape_dtype passes, the code continues, and then we run into the AttributeError.

        xp = _check_ns_shape_dtype(actual, desired, check_dtype, check_shape, check_scalar)
    
       floating = xp.isdtype(actual.dtype, ("real floating", "complex floating"))
       # AttributeError: module 'sparse' has no attribute 'isdtype'. Did you mean: 'astype'?

So whether the test will fail with sparse arrays due to the AttributeError depends on check_shape/check_dtype. We cannot simply use the param_assert_equal_close decorator to xfail (strict) the sparse tests, nor can we remove it.

So what would you prefer:

  • Mark the test with pytest.xfail inside the test function considering the value of check_shape/check_dtype (in addition to the backend)
  • Create some alternative to param_assert_equal_close that effectively does the same thing as the previous option (but is harder for me to write)
  • Make the XFAIL with sparse arrays not strict (perhaps I can figure out why strict=False was not enough?)
  • Just skip the test with sparse arrays
  • Remove the parameterization and write two separate tests just to accommodate for sparse arrays
  • Something else?

If it were me, I would just do option 1. I have no qualms about using pytest.xfail within a function, but since someone went through the trouble of creating param_assert_equal_close, it seems that is not desirable to everyone.

@lucascolley
Copy link
Member

lucascolley commented Apr 16, 2025

Multiple of those options seem fine with me (at least for now), but I'll ping @crusaderky as the original author first

@lucascolley lucascolley changed the title WIP: xp_assert_ enhancements WIP: ENH: xp_assert_ enhancements Apr 16, 2025
@lucascolley lucascolley changed the title WIP: ENH: xp_assert_ enhancements WIP: ENH/TST: xp_assert_ enhancements Apr 16, 2025
@hameerabbasi
Copy link

The failures are XPASSes: [XPASS(strict)] sparse:no isdtype

Judging by the message, we're expecting the test to fail due to sparse not implementing isdtype, but it is now passing. Could that be because of any of your changes?

We recently released 0.16.0; I believe that may have added isdtype.

@mdhaber
Copy link
Contributor Author

mdhaber commented Apr 17, 2025

@hameerabbasi If so, it does not appear to be documented at https://sparse.pydata.org/en/stable/api/. Is that where I should be looking?

Also - I'm not very familiar with the sparse project - is there somewhere that summarizes the current relationship between scipy.sparse and sparse projects?

@hameerabbasi
Copy link

Whoops: It's in a non-standard experimental part of the codebase; not exposed to the user. My bad.

@lucascolley
Copy link
Member

is there somewhere that summarizes the current relationship between scipy.sparse and sparse projects?

https://labs.quansight.org/blog/sparse-array-ecosystem is probably a good start

Comment on lines 79 to 81
assert (np.isscalar(actual) and np.isscalar(desired)) or (
not np.isscalar(actual) and not np.isscalar(desired)
), _msg
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert (np.isscalar(actual) and np.isscalar(desired)) or (
not np.isscalar(actual) and not np.isscalar(desired)
), _msg
assert np.isscalar(actual) == np.isscalar(desired), _msg

Comment on lines +127 to +129
check_scalar : bool, default: False
NumPy only: whether to check agreement between actual and desired types -
0d array vs scalar.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default for this is the opposite as in scipy

Copy link
Contributor Author

@mdhaber mdhaber Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant to mention that, so thanks for bringing it up. I think it should be True, but that would make a lot of tests fail internally. I figured that could be fixed in a follow-up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

array-api-extra tests? If so, yes, that sounds fine, but we should open an issue for that before merging this

Copy link
Contributor Author

@mdhaber mdhaber Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Sure, I'll open an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err_msg=err_msg,
)
# JAX/Dask arrays work directly with `np.testing`
assert isinstance(rtol, float)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isinstance(0, float) returns False

Suggested change
assert isinstance(rtol, float)
assert isinstance(rtol, int | float)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine, but that code was here - do we even need it?

Copy link
Contributor

@crusaderky crusaderky Apr 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't here. Maybe in scipy? Agree that the assert is overkill.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was:

assert isinstance(rtol, float)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stand corrected. 🙏

Comment on lines +28 to +30
marks=pytest.mark.xfail_xp_backend(
Backend.SPARSE, reason="no isdtype", strict=False
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#269 adds support for strict=False, which was previously quietly ignored.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now merged

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used.

Comment on lines +57 to +58
actual_shape = actual.shape
desired_shape = desired.shape
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may fail if we start using it in scipy, because scipy overrides array_namespace to return numpy for scalars and lists. Maybe out of scope for this PR I though?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh. Yeah let's do l consider that in a SciPy PR that attempts to use this there. Then we can decide whether/what changes are needed.

Comment on lines +28 to +30
marks=pytest.mark.xfail_xp_backend(
Backend.SPARSE, reason="no isdtype", strict=False
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#269 adds support for strict=False, which was previously quietly ignored.

Copy link
Contributor Author

@mdhaber mdhaber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaving the pyright failures so you can decide how you want to deal with them. I think all of them are false positives, as usual with static type checking.

Comment on lines +127 to +129
check_scalar : bool, default: False
NumPy only: whether to check agreement between actual and desired types -
0d array vs scalar.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import numpy as np # pylint: disable=import-outside-toplevel

# JAX/Dask arrays work directly with `np.testing`
assert isinstance(rtol, float)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was probably added to avoid the pyright error below? I don't think pyright should make us do this sort of thing.

Comment on lines +28 to +30
marks=pytest.mark.xfail_xp_backend(
Backend.SPARSE, reason="no isdtype", strict=False
),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used.

else nullcontext()
)
with context:
func(xp.asarray([xp.nan, xp.nan]), xp.asarray(xp.nan), check_shape=check_shape)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NaNs pass NumPy's _close, _equal, and _less checks, so using NaNs here (and below) allows us to parametrize over all three functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants