Skip to content
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

Issue #3473 #3474

Closed
wants to merge 7 commits into from
Closed

Issue #3473 #3474

wants to merge 7 commits into from

Conversation

Drew-Ack
Copy link

A small change to how pytest.approx works.
This is to address issue #3473

@coveralls
Copy link

coveralls commented May 14, 2018

Coverage Status

Coverage increased (+0.05%) to 92.803% when pulling 2c0c161 on Drew-Ack:Issue_#3473 into 36614b0 on pytest-dev:master.

@maiksensi
Copy link
Contributor

Would it also make sense to add a small test, checking against the expected type error when passing in a string?

@Drew-Ack
Copy link
Author

Drew-Ack commented May 14, 2018

There are two tests I could write.

  1. Test checks to see that a TypeError is correctly raised when trying to use a string with approx.
  2. Test uses the xfail mark. This test would be try to use a string with approx, which is expected to fail, for now.

@RonnyPfannschmidt
Copy link
Member

xfail is for tests you want to fix, this is a test for a exception condition that is supposed to happen - so use pytest.raises

@maiksensi
Copy link
Contributor

Do you consider the first test a unit test and the second one an integration (where you actually try to run a test that uses approx with a string? If so both tests sound useful. As Ronny said, using pytest.raises probably makes more sense for the second test.
I am not too opinionated about how many tests need to be added. In any case I think it would already help to check for correctly raising the TypeError when calling approx with a string.
If you need any help with testing the behavior let me know.

… that a TypeError is raised correctly when a string is passed into approx().
@Drew-Ack
Copy link
Author

Drew-Ack commented May 15, 2018

Added a single test case that uses pytest.raises as @RonnyPfannschmidt suggested.
@maiksensi I thought about a second test case, but they're so similar that one is enough for this behaviour.

@nicoddemus
Copy link
Member

Thanks @Drew-Ack, we appreciate you taking the time to write a PR!

This fixes #3473.

@@ -443,8 +443,9 @@ def approx(expected, rel=None, abs=None, nan_ok=False):
# This has the advantage that it made it easy to support mapping types
# (i.e. dict). The old code accepted mapping types, but would only compare
# their keys, which is probably not what most people would expect.

if _is_numpy_array(expected):
if isinstance(expected, String):
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps it would be better if we change the else: clause here to check for float/int, and then add an else: clause which fails with everything else?

    elif isinstance(expected, Decimal):
        cls = ApproxDecimal
    elif isinstance(expected, (float,) + six.integer_types):
        cls = ApproxScalar
    else:
        raise TypeError('Unknown type passed to approx: {!r} ({!r})'.format(expected, type(expected))

Copy link
Author

Choose a reason for hiding this comment

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

But isn't string the only thing that cannot currently be approx'd?

Copy link
Author

Choose a reason for hiding this comment

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

Nevermind! I did some tests and noticed it doesnt accept things I thought it could.


    if _is_numpy_array(expected):
        cls = ApproxNumpy
    elif isinstance(expected, Mapping):
        cls = ApproxMapping
    elif isinstance(expected, Sequence) and not isinstance(expected, String):
        cls = ApproxSequence
    elif isinstance(expected, Decimal):
        cls = ApproxDecimal
    elif isinstance(expected, (float,) + six.integer_types):
        cls = ApproxScalar
    else:
       raise TypeError('Unknown type passed to approx: {!r} ({!r})'.format(expected, type(expected))

Would be the final commit correct?

Im not sure how that gets us from passing in [6, (5,4)] though. Because the above clauses would see it as valid, but math.isinf freaks out about it (rightly so).

Copy link
Member

Choose a reason for hiding this comment

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

Hey @Drew-Ack, thanks for replying so quickly!

Im not sure how that gets us from passing in [6, (5,4)] though. Because the above clauses would see it as valid, but math.isinf freaks out about it (rightly so).

I'm not sure I follow, could you clarify please?

Copy link
Author

@Drew-Ack Drew-Ack May 16, 2018

Choose a reason for hiding this comment

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

No problem! Love pytest afterall :)

As an example this elif:

    elif isinstance(expected, Sequence) and not isinstance(expected, String):
        cls = ApproxSequence

allows this [5, (6,7)] as valid input into the approx function. This technically shouldnt be, because even though its a sequence it will just blow up when appox runs.

The issue comes when math.isinf runs. math.isinf is evaluated in the ApproxSequence class. I show this in #3473

I assume this clause is used purely to delegate which class to use, but provides no checking to make sure the sequence is valid.
Is this something we should try to do?
Should we throw an exception out when an invalid sequence like [5, (6,7)] is given

Copy link
Member

Choose a reason for hiding this comment

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

allows this [5, (6,7)] as valid input into the approx function.

Oh my bad, it was obvious you meant [5, (6, 7)] the literal. Sorry about that.

This technically shouldnt be, because even though its a sequence it will just blow up when appox runs.

I see. I guess we can iterate over every element of Sequence and Mapping to ensure their elements are float or int objects; there might be a performance problem but I don't think people are using pytest.approx with arrays so large that this would make a difference.

Copy link
Author

@Drew-Ack Drew-Ack May 16, 2018

Choose a reason for hiding this comment

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

This is the first open source project I've contributed too, so I will defer to you developers on what is best practice.

It seems to me we are just picking what point to throw an exception at.
I put together a small test case to see how long it would take :)

def test_time():
    mylist = []
    for x in range(1000000):
        mylist.append(x)

    start_time = time.time()

    for x in mylist:
        if isinstance(x, float) or isinstance(x, int):
            continue

    end_time = time.time()

    print(end_time - start_time)

Its roughly .28 seconds to run through 1 million elements.
Its roughly 2.8 seconds to run through 10 million.
The pattern holds as you increase by x10

Copy link
Member

@nicoddemus nicoddemus May 16, 2018

Choose a reason for hiding this comment

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

This is the first open source project I've contributed too, so I will defer to you developers on what is best practice.

This is great, thanks for contributing to pytest, we definitely appreciate it!

What do you think @RonnyPfannschmidt? I think we can get away by keeping things simple and just going with my suggestion in #3474 (comment), but I don't oppose if others prefer to be more through.

Copy link
Member

Choose a reason for hiding this comment

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

basically massive sequences shouldn't use python lists ^^ so the check should be reasonably fine for normal sequences

Copy link
Member

Choose a reason for hiding this comment

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

OK, @Drew-Ack could go with a more through check for sequences then? Thanks!

@Drew-Ack
Copy link
Author

I am unsure why the AppVeyor build failed.

@maiksensi
Copy link
Contributor

I am unsure why the AppVeyor build failed.

Looks like a version conflict with twisted. What is the pytest-trail 2.7 run actually doing?

@@ -450,6 +450,9 @@ def approx(expected, rel=None, abs=None, nan_ok=False):
elif isinstance(expected, Mapping):
cls = ApproxMapping
elif isinstance(expected, Sequence) and not isinstance(expected, String):
for value in expected:
if isinstance(value, (Sequence, String, Mapping)):
Copy link
Member

Choose a reason for hiding this comment

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

I believe it is better to do check for numbers, and provide a better message to locate the problem:

        number_types = six.integer_types + (float,)
        for index, value in enumerate(expected):
            if not isinstance(value, number_types):
				msg = 'Sequence must contain only numbers, but found {!r} ({!r}) at index {}'
                raise TypeError(msg.format(value, type(value), index))

Copy link
Member

Choose a reason for hiding this comment

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

We also should add a test for this 😉

Copy link
Author

@Drew-Ack Drew-Ack May 18, 2018

Choose a reason for hiding this comment

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

I cant just check for int, float, and long though. I would also have to check for each numpy type too. I do agree that a better message should be there :)

Copy link
Author

Choose a reason for hiding this comment

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

@nicoddemus How should I approach numpy types?

Copy link
Member

Choose a reason for hiding this comment

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

There's already a elif _is_numpy_array(expected): check near the start of the if/elif chain, so numpy.array are covered already.

There's the situation where the user passes a builtin list containing numpy numbers, but that seems rare to me. So we can either:

  1. Also check every possible number type, including numpy's:

    # _get_numpy_types() returns a tuple (numpy.int32, numpy.int64, ...) if numpy is available,
    # otherwise an empty tuple
    number_types = six.integer_types + (float,) + self._get_numpy_types()  
  2. Consider this overkill and just fail when we encounter a list containing numpy types. In this case
    should make sure to show the data type in the error message (like in my previous example).

I think it is worthwhile trying to go with 1), this will make things more convenient for our users at the cost of a little more
code.

What do you folks think?

@@ -450,6 +450,9 @@ def approx(expected, rel=None, abs=None, nan_ok=False):
elif isinstance(expected, Mapping):
cls = ApproxMapping
elif isinstance(expected, Sequence) and not isinstance(expected, String):
for value in expected:
if isinstance(value, (Sequence, String, Mapping)):
Copy link
Member

Choose a reason for hiding this comment

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

We also should add a test for this 😉

@nicoddemus
Copy link
Member

@Drew-Ack gentle ping. 😁

@blueyed
Copy link
Contributor

blueyed commented Aug 25, 2018

#3473 was closed via #3741 already.
I guess this needs a rebase at least - I have not checked if there is something left here then.

@nicoddemus
Copy link
Member

Oh right, thanks @blueyed.

It seems indeed that #3741 already implemented the type checking we wanted here, so I'm closing this.

@Drew-Ack feel free to ping/reopen if we are missing something. 👍

@nicoddemus nicoddemus closed this Aug 25, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants