Skip to content

Conversation

@symonk
Copy link
Member

@symonk symonk commented Jul 27, 2021

Hi all, hope you are well :) I have an initially poc at refactoring the pytest.skip(msg=...) syntax in favour of a reason= arg in the sig, I have attempted to deprecate this for now in the simplest way I can think possible. I have ran out of time tonight so opened this PR for a discussion in the interim.

I am not familiar with how we deprecate things in general here in pytest, especially not function signature args. Please let me know what you think, if i'm on the right track etc.

We have a few circular imports I need to spend more time on, we should maybe also consider OutcomeException using a reason attr itself, however I'm aware there are other use cases that currently use msg=.

closes #8948

This needs a lot of work, opening it as a draft

@symonk
Copy link
Member Author

symonk commented Jul 27, 2021

@The-Compiler @RonnyPfannschmidt @nicoddemus opened a draft here, I am unsure the best approach for something like this so I welcome any feed back, I have ran out of time this evening so I thought I would try get a conversation started around it for me to pick up later and tweak, few queries:

  • Should OutcomeException attr be reason
  • I'll need to investigate the circle imports going on here around the deprecation imports
  • Another outlier with msg= I think its pytest.fail will I include that and unify across the board?
  • Is there a better way to deprecate this than my approach?
  • Any other tests you'd like to see added?
  • Update docs
  • Deprecate all msg= functions
  • Change log

Copy link
Member

@RonnyPfannschmidt RonnyPfannschmidt left a comment

Choose a reason for hiding this comment

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

great start 👍 , please address the minor nitpicks

@symonk symonk marked this pull request as ready for review July 28, 2021 19:55
@symonk
Copy link
Member Author

symonk commented Jul 28, 2021

I have excluded exit(msg=...) on purpose here for now pending core feedback on it

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

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

@symonk thanks for the PR, great work!

I left some minor comments, please take a look.

Also, it is important to add a section to doc/en/deprecations.rst mentioning the deprecation, rationale, and how to update existing code. After doing that, add a link to the new section to the changelog/8948.deprecation.rst file.

@symonk symonk changed the title Deprecating pytest.skip(msg=...) in favour of pytest.skip(reason=...) Deprecation of msg= for both pytest.skip() and pytest.fail(). Jul 30, 2021
@symonk
Copy link
Member Author

symonk commented Jul 30, 2021

@symonk thanks for the PR, great work!

I left some minor comments, please take a look.

Also, it is important to add a section to doc/en/deprecations.rst mentioning the deprecation, rationale, and how to update existing code. After doing that, add a link to the new section to the changelog/8948.deprecation.rst file.

As always, thanks again! appreciate it.

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

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

Looks great, thanks! Left two minor changes, otherwise LGTM!

symonk and others added 2 commits July 30, 2021 23:15
Co-authored-by: Bruno Oliveira <[email protected]>
Co-authored-by: Bruno Oliveira <[email protected]>
@symonk
Copy link
Member Author

symonk commented Jul 31, 2021

pre-commit.ci run

@symonk
Copy link
Member Author

symonk commented Aug 2, 2021

thoughts on getting this in for 7.0? would let us be uniform by 8.1 in this regard atleast - Thoughts on pytest.exit(msg...) - I am happy to integrate it here but i wasen't sure if we want that

@nicoddemus
Copy link
Member

I'm fine with getting this for 7.0. 👍

@The-Compiler
Copy link
Member

Fine by me, given that 7.0 is delayed a bit more anyways (I intend to pick it up again in 2-3 weeks after I've had some holidays)

@symonk
Copy link
Member Author

symonk commented Aug 3, 2021

please ignore spending any time looking at this until I sort it out, as soon as possible :) I'm going to implement deprecation as a decorator to handle all 3 cases, pytest.exit() having a non optional msg arg made things extra tricky (maybe im just over thinking it) and I want to handle all 3 cases in a clean manner

@nicoddemus
Copy link
Member

'm going to implement deprecation as a decorator to handle all 3 cases, pytest.exit() having a non optional msg arg made things extra tricky (maybe im just over thinking it) and I want to handle all 3 cases in a clean manner

Cool, thanks!

Also reminder to then mention pytest.exit in the docs too.

@symonk
Copy link
Member Author

symonk commented Aug 5, 2021

'm going to implement deprecation as a decorator to handle all 3 cases, pytest.exit() having a non optional msg arg made things extra tricky (maybe im just over thinking it) and I want to handle all 3 cases in a clean manner

Cool, thanks!

Also reminder to then mention pytest.exit in the docs too.

will do, got it done locally just need to test my changes but I've had no time the past 2 days, hopefully do it tonight because i'm away this weekend :)

@symonk
Copy link
Member Author

symonk commented Aug 5, 2021

@nicoddemus any advice on how best to handle the exit() case, by default it is msg: str (non optional) which is a little trickier, is it acceptable to change that sig to reason: str = "" and apply the same solution as the others here?

alternatively, all I can think of is something like a decorator like:

def deprecate_msg_for_reason(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        if "msg" in kwargs:
            kwargs["reason"] = kwargs["msg"]
            del kwargs["msg"]
            warnings.warn(KEYWORD_MSG_ARG.format(func=f.__name__), stacklevel=2)
            return f(*args, **kwargs)
    return wrapper

Curious on some feed back before i spend time implementing it, the problem with the decorator approach is it leaves a lackluster sig where an arg is probably defined but unused, or like:

def exit(reason: str, returncode: Optional[int] = None, **kwargs) -> "NoReturn":
def exit(reason: str, returncode: Optional[int] = None, msg: Optional[str] = None) -> "NoReturn":

which feels bad to me, in the decorator approach neither are used in the function scope

@nicoddemus
Copy link
Member

Hey @symonk

is it acceptable to change that sig to reason: str = "" and apply the same solution as the others here?

I think so, any reason you feel this should be handled differently?

@symonk
Copy link
Member Author

symonk commented Aug 5, 2021

Thanks @nicoddemus I wasen't sure if there was a reason why pytest.exit() enforced a msg from the user w/o a default, i guess not? :)

edit: That makes my life a little easier! I was cautious of changing as I assumed there was a reason behind why it was non optional

@nicoddemus
Copy link
Member

Ahh I see! Sorry, now I get why you asked.

But we need to have a message associated with it, otherwise we don't have a reason to show.

How about this?

def exit(reason: str="", *, msg: Optional[str]=None):
    if reason and msg:
        raise UsageError("Cannot pass both reason and msg to exit")
    if not reason:
        if msg is None:
            raise UsageError("exit() needs a reason argument")
        warnings.warn("msg is deprecated, use reason instead")    
        reason = msg
    # use `reason`

We can then explain in the docstring that reason has a default value only because msg is deprecated, but exit needs one argument.

@symonk
Copy link
Member Author

symonk commented Aug 5, 2021

Ahh I see! Sorry, now I get why you asked.

But we need to have a message associated with it, otherwise we don't have a reason to show.

How about this?

def exit(reason: str="", *, msg: Optional[str]=None):
    if reason and msg:
        raise UsageError("Cannot pass both reason and msg to exit")
    if not reason:
        if msg is None:
            raise UsageError("exit() needs a reason argument")
        warnings.warn("msg is deprecated, use reason instead")    
        reason = msg
    # use `reason`

We can then explain in the docstring that reason has a default value only because msg is deprecated, but exit needs one argument.

I like it, i will keep the reuse for skip/fail as is, but i did wonder if calling fail() for the exit scenario would cause a problem, this is a happy middle ground 👍

@symonk
Copy link
Member Author

symonk commented Aug 5, 2021

out of time tonight, need to look into a few linting oddities in:

src/_pytest/scope.py:62: error: Missing return statement  [return]
Found 1 error in 1 file (checked 4 source files)

ailing in CI too, kinda puzzled as i haven't touch that file, i did change fail() tho ofc, maybe to do with that, will chase that up later

and tidy up a few loose ends, will prompt for review when I finish up, thanks again.

mini task list for later:

  • fix linter
  • review docs
  • review test coverage
  • review calling fail() inside a deprecated pytest.skip() scenario

if msg is not None:

if reason:
fail(
Copy link
Member Author

Choose a reason for hiding this comment

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

is this technically correct for the skip scenario? not sure.

Copy link
Member

Choose a reason for hiding this comment

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

To call fail you mean?

Hmmm you probably should not call fail, but raise UsageError.

@The-Compiler
Copy link
Member

What's the state of this? I plan to get back to a 7.0 rc1 soon (possibly next week), and if this doesn't make it into the RC, I don't think we should have it in the 7.0 final either.

@symonk
Copy link
Member Author

symonk commented Aug 27, 2021

@The-Compiler I will set aside some time tomorrow morning and finish this off, I have some family stuff on after work this evening, hopefully it will be wrapped up by midday or so tomorrow.

edit: more than expected to sort when bringing main in ^^, working on it

)
# I don't know why mypy complains here, `fail()` is typed as `typing.NoReturn`
# yet it complains of error: Missing return statement [return] ?
assert False, "unreachable"
Copy link
Member Author

Choose a reason for hiding this comment

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

Anyone savvy with mypy knows why it complains of a missing return here? the Scope instantiation is correctly catching the ValueError and fail() has a forward ref return type of "No Return". This seems unnecessary? to me

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Looks like a mypy bug or at least something mypy can improve. Maybe the "missing return value" analysis does not consider unreachability (which is correctly detected in this case, I verified this by adding some statement after the fail and seeing that mypy complains that the statement is unreachable).

If you want to avoid the assert you can replace return ... with scope = ... and return scope outside of the try-except. Slightly less clean but OK I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

Whatever you folks think is best I will happily implement, kind of a rock and a hard place here :)

  • keep assert as is
  • hold a reference scope and return it after the except

(may have some issues around local variables referenced before assignment that won't break anything in the latter)

In the mean time I will try recreate and open a mypy issue (if one is required)

Copy link
Member

Choose a reason for hiding this comment

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

I'd go with "hold a reference scope and return it after the except".

Reported to mypy would be nice, if you are able to reduce it to a standalone example. You should check if it has already been reported though.

Copy link
Member Author

Choose a reason for hiding this comment

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

@bluetech Will do! thanks

@symonk
Copy link
Member Author

symonk commented Aug 28, 2021

Moved to a UsageError instead of calling fail for bad args, Got a query on a mypy assert I had to add to make it work (which seems unnecessary to me, can't quite put my foot on why its complaining inside scope.py here. Will address code cov / outliers shortly

@symonk symonk requested a review from nicoddemus August 28, 2021 09:49
Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

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

LGTM, thanks a lot @symonk!

@symonk
Copy link
Member Author

symonk commented Aug 30, 2021

@nicoddemus @RonnyPfannschmidt @The-Compiler @bluetech - Thanks as always for all the feed back here, I think it is good to go (I've applied Rans latest suggestion), would be good to wrap this up for 7.0. If there is any other pressing issues we need some resource to look at as priority please send it my way.

Copy link
Member

@The-Compiler The-Compiler left a comment

Choose a reason for hiding this comment

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

Phew, this turned out quite a bit more complex than I had imagined! Great work @symonk, thanks for the determination! 👍

@The-Compiler
Copy link
Member

@RonnyPfannschmidt I think your review comments have all been addressed, correct?

@RonnyPfannschmidt
Copy link
Member

Indeed

@The-Compiler The-Compiler merged commit eb6c449 into pytest-dev:main Nov 8, 2021
@The-Compiler
Copy link
Member

Let's get this in then, thanks everyone!

@symonk symonk deleted the deprecate-skip-msg-for-reason branch November 8, 2021 14:39
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.

pytest.skip: Rename "msg" to "reason" for consistency with pytest.mark.skip/xfail?

5 participants