8000 API Wrap __new__ instead of __init__ with the deprecated decorator by jeremiedbb · Pull Request #25733 · scikit-learn/scikit-learn · GitHub
[go: up one dir, main page]

Skip to content

API Wrap __new__ instead of __init__ with the deprecated decorator #25733

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

Merged
merged 5 commits into from
Mar 8, 2023

Conversation

jeremiedbb
Copy link
Member

Fixes #15994
Closes #15995

Although the original issue an PR are old, I think it's still relevant for third party developers.

When a class inherits from a deprecated class but completely overrides the __init__ method, the FutureWarning is not raised.
This PR proposes to raise the warning in __new__ instead as an old comment suggested. Raising in both methods is also a possibility but then we would emit the warning twice which is not very user friendly.

Co-authored-by: Brigitta Sipőcz <brigitta.sipocz@gmail.com>
@jeremiedbb jeremiedbb added the Quick Review For PRs that are quick to review label Mar 1, 2023
@jeremiedbb jeremiedbb changed the title deprecate new instead of init API Deprecate __new__ instead of __init__ with the deprecated decorator Mar 1, 2023
@jeremiedbb jeremiedbb changed the title API Deprecate __new__ instead of __init__ with the deprecated decorator API Wrap __new__ instead of __init__ with the deprecated decorator Mar 1, 2023
Copy link
Member
@thomasjpfan thomasjpfan left a comment

Choose a reason for hiding this comment

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

Thanks for the PR! Do we need to change anything here?

cls_init = getattr(cls, "__init__", None)
if _is_deprecated(cls_init):
continue

@adrinjalali
Copy link
Member

I'm going to test this against skops.io to see if it breaks anything, we call __new__ there and we hope for it not to do much.

@jeremiedbb
Copy link
Member Author
jeremiedbb commented Mar 3, 2023

@adrinjalali please check with the last commit because I just pushed a fix

@jeremiedbb
Copy link
Member Author
jeremiedbb commented Mar 3, 2023

Do we need to change anything here?

Good catch, we do. And it actually found a bug. When overriding new, only the class can be passed.
I fixed it and extended the deprecation tests to cover that

@adrinjalali
Copy link
Member

skops's tests pass on this.

warnings.warn(msg, category=FutureWarning)
return new(*args, **kwargs)
return new(cls)
Copy link
Member

Choose a reason for hiding this comment

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

If *args are not passed up, then the following fails:

@deprecated("a message")
class MockClass6:
    def __new__(cls, *args, **kwargs):
        assert args[0] == 42
        # if super() is object, then only `cls` can be passed up
        return super().__new__(cls)

    def __init__(self, a):
        self.a = a

def test_deprecated():
    with pytest.warns(FutureWarning, match="a message"):
        MockClass6(42)

I do not see a great way around it besides catching the error and trying again:

def wrapped(cls, *args, **kwargs):
    warnings.warn(msg, category=FutureWarning)
    try:
        return new(cls, *args, **kwargs)
    except TypeError:
        # object accepts a single argument
        return new(cls)

Copy link
Member

Choose a reason for hiding this comment

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

This makes me wonder if we really want to move this logic to __new__. It's adding complexity somewhere else, only hoping weird things don't happen to __new__ in the child class I guess.

Since this is about deprecations and changes in parameters, should we just mark the original issue as "won't fix" and expect third party developers to handle it themselves?

Copy link
Member Author
@jeremiedbb jeremiedbb Mar 6, 2023

Choose a reason for hiding this comment

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

Last attempt: I pushed a new commit which should be safe.

It's adding complexity somewhere else, only hoping weird things don't happen to new in the child class I guess.

I wouldn't say "adding" but "moving". The current version in main already suffers from the same kind of issue:

@deprecated("a message")
class MockClass6:
    def __new__(cls, a):
        print("a", a)
        return super().__new__(cls)

MockClass6(42)

Copy link
Member Author

Choose a reason for hiding this comment

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

However, if you still find this too brittle, I'm okay to close this PR and the original issue as won't fix.

Copy link
Member
@adrinjalali adrinjalali left a comment

Choose a reason for hiding this comment

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

I played around with this a bit, I think I'm okay with the current implementation, but I'll let @thomasjpfan have another look.

Copy link
Member
@thomasjpfan thomasjpfan left a comment

Choose a reason for hiding this comment

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

I think this is okay. I could not find a way to break it. LGTM

@thomasjpfan thomasjpfan merged commit 1dc0dd4 into scikit-learn:main Mar 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module:utils Quick Review For PRs that are quick to review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

FutureWarning is not issued for deprecated class
4 participants
0